├── .DS_Store ├── .env.example ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── analysis_options.yaml ├── bin └── backend.dart ├── database ├── database.dart └── migrations │ └── 2024_04_20_003612_initial_setup.dart ├── lib ├── app │ ├── middlewares │ │ ├── api_auth_middleware.dart │ │ ├── core_middleware.dart │ │ └── middlewares.dart │ ├── providers │ │ ├── provide_blog_services.dart │ │ ├── provide_core.dart │ │ ├── provide_database.dart │ │ ├── provide_routes.dart │ │ └── providers.dart │ └── routes │ │ ├── api.dart │ │ └── web.dart ├── backend.dart └── src │ ├── controllers │ ├── article_controller.dart │ ├── auth_controller.dart │ ├── controllers.dart │ └── user_controller.dart │ ├── dto │ ├── article_dto.dart │ ├── dto.dart │ └── user_dto.dart │ ├── models.dart │ ├── services │ ├── article_service.dart │ ├── auth_service.dart │ └── services.dart │ └── utils │ ├── config.dart │ └── utils.dart ├── melos.yaml ├── melos_backend.iml ├── melos_dart_blog.iml ├── packages ├── frontend │ ├── .fvmrc │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── lib │ │ ├── auth │ │ │ ├── auth.dart │ │ │ ├── auth_header.dart │ │ │ └── pages │ │ │ │ ├── auth_layout.dart │ │ │ │ ├── login_page.dart │ │ │ │ └── register_page.dart │ │ ├── blog │ │ │ ├── blog.dart │ │ │ ├── blog_detail.dart │ │ │ └── widgets │ │ │ │ ├── add_article_card.dart │ │ │ │ ├── article_base_layout.dart │ │ │ │ ├── article_card.dart │ │ │ │ ├── article_form.dart │ │ │ │ └── article_items.dart │ │ ├── data │ │ │ ├── api_service.dart │ │ │ └── providers │ │ │ │ ├── article_provider.dart │ │ │ │ └── auth_provider.dart │ │ ├── main.dart │ │ └── utils │ │ │ ├── misc.dart │ │ │ └── provider.dart │ ├── pubspec.lock │ ├── pubspec.yaml │ └── web │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── icons │ │ ├── Icon-maskable-192.png │ │ ├── Icon-maskable-512.png │ │ ├── icon-192.png │ │ └── icon-512.png │ │ ├── index.html │ │ └── manifest.json └── shared │ ├── .DS_Store │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── analysis_options.yaml │ ├── lib │ ├── models.dart │ ├── shared.dart │ └── src │ │ └── models │ │ ├── article.dart │ │ └── user.dart │ ├── melos_shared.iml │ └── pubspec.yaml ├── pubspec.lock ├── pubspec.yaml ├── pubspec_overrides.yaml ├── screenshot.png └── test └── backend_test.dart /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/.DS_Store -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PEXELS_API_KEY= 2 | 3 | # Required for production 4 | DB_HOST= 5 | DB_USERNAME= 6 | DB_DATABASE= 7 | DB_PASSWORD= 8 | 9 | # Required for generating & validating auth tokens 10 | APP_KEY= -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Test 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | branches: ["main"] 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze Code 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v3 21 | 22 | - uses: dart-lang/setup-dart@v1.3 23 | - uses: subosito/flutter-action@v2 24 | with: 25 | channel: stable 26 | - uses: bluefireteam/melos-action@v3 27 | 28 | 29 | - name: Check formatting 30 | run: dart format . --line-length=120 --set-exit-if-changed 31 | 32 | - name: Check linting 33 | run: | 34 | dart run build_runner build 35 | cd packages/shared && dart run build_runner build --delete-conflicting-outputs 36 | dart analyze . --fatal-infos 37 | 38 | test: 39 | name: Run Tests 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout Repository 43 | uses: actions/checkout@v3 44 | 45 | - uses: dart-lang/setup-dart@v1.3 46 | - uses: subosito/flutter-action@v2 47 | with: 48 | channel: stable 49 | - uses: bluefireteam/melos-action@v3 50 | 51 | - name: Bootstrap 52 | run: | 53 | dart pub global activate coverage 54 | dart run build_runner build --delete-conflicting-outputs 55 | cd packages/shared && dart run build_runner build --delete-conflicting-outputs 56 | 57 | - name: Setup Test Database 58 | run: dart run yaroorm_cli migrate --connection=local 59 | 60 | - name: Run Tests 61 | run: | 62 | dart test --coverage=coverage --fail-fast 63 | dart pub global run coverage:format_coverage --check-ignore --report-on=lib --lcov -o lcov.info -i ./coverage 64 | 65 | - name: Upload Coverage 66 | uses: codecov/codecov-action@v3 67 | env: 68 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 69 | with: 70 | files: lcov.info 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | *.g.dart 3 | *.sqlite 4 | 5 | 6 | _yoorm_tool/ 7 | *.reflectable.dart 8 | 9 | # app 10 | .env 11 | storage/web/.session 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.lineLength": 120, 3 | "[dart]": { 4 | "editor.rulers": [ 5 | 120 6 | ], 7 | } 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dart Blog Backend 2 | 3 | ![dart](https://github.com/codekeyz/yaroo-example/actions/workflows/test.yml/badge.svg) [![codecov](https://codecov.io/gh/codekeyz/yaroo-example/graph/badge.svg?token=Q3YPK3LRLR)](https://codecov.io/gh/codekeyz/yaroo-example) 4 | 5 | ![Dart Blog Dashbaord](./screenshot.png) 6 | 7 | ### Setup 8 | 9 | ```shell 10 | dart pub get && dart run build_runner build --delete-conflicting-outputs 11 | ``` 12 | 13 | ### Migrate Database 14 | 15 | - For local dev, execute migrations on sqlite database using the command below 16 | 17 | ```shell 18 | dart run yaroorm_cli migrate --connection=local 19 | ``` 20 | 21 | - For production database, you can run this. 22 | 23 | ```shell 24 | dart run yaroorm_cli migrate 25 | ``` 26 | 27 | ```shell 28 | ┌───────────────────────────────┬──────────────────────────────┐ 29 | │ Migration │ Status │ 30 | ├───────────────────────────────┼──────────────────────────────┤ 31 | │ initial_table_setup │ ✅ migrated │ 32 | └───────────────────────────────┴──────────────────────────────┘ 33 | ``` 34 | 35 | ### Start Server 36 | 37 | ```shell 38 | dart run 39 | ``` 40 | 41 | ### Tests 42 | 43 | ```shell 44 | dart test 45 | ``` 46 | 47 | ### Contribution & Workflow 48 | 49 | We rely heavily on code-generation. Things like adding a new `Entity`, `Middleware`, `Controller` or `Controller Method` 50 | require you to re-run the command below. 51 | 52 | ```shell 53 | dart pub run build_runner build --delete-conflicting-outputs 54 | ``` 55 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | 4 | linter: 5 | rules: 6 | file_names: false 7 | 8 | analyzer: 9 | exclude: 10 | - "**.reflectable.dart" 11 | - frontend/ 12 | # For more information about the core and recommended set of lints, see 13 | # https://dart.dev/go/core-lints 14 | 15 | # For additional information about configuring this file, see 16 | # https://dart.dev/guides/language/analysis-options 17 | -------------------------------------------------------------------------------- /bin/backend.dart: -------------------------------------------------------------------------------- 1 | import 'package:backend/backend.dart'; 2 | 3 | import 'backend.reflectable.dart'; 4 | 5 | import '../database/database.dart' as db; 6 | 7 | void main(List arguments) async { 8 | initializeReflectable(); 9 | db.initializeORM(); 10 | 11 | await blogApp.bootstrap(); 12 | } 13 | -------------------------------------------------------------------------------- /database/database.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: no_leading_underscores_for_library_prefixes 4 | import 'package:backend/src/models.dart'; 5 | import 'package:backend/src/utils/config.dart' as config; 6 | import 'package:yaroorm/yaroorm.dart'; 7 | 8 | import 'migrations/2024_04_20_003612_initial_setup.dart' as _m0; 9 | 10 | void initializeORM() { 11 | /// Add Type Definitions to Query Runner 12 | Query.addTypeDef(serveruserTypeDef); 13 | Query.addTypeDef(serverarticleTypeDef); 14 | 15 | /// Configure Migrations Order 16 | DB.migrations.addAll([ 17 | _m0.InitialTableSetup(), 18 | ]); 19 | 20 | DB.init(config.config); 21 | } 22 | -------------------------------------------------------------------------------- /database/migrations/2024_04_20_003612_initial_setup.dart: -------------------------------------------------------------------------------- 1 | import 'package:backend/src/models.dart'; 2 | import 'package:yaroorm/yaroorm.dart'; 3 | 4 | class InitialTableSetup extends Migration { 5 | @override 6 | void up(List schemas) { 7 | schemas.addAll([ServerUserSchema, ServerArticleSchema]); 8 | } 9 | 10 | @override 11 | void down(List schemas) { 12 | schemas.addAll([ 13 | Schema.dropIfExists(ServerUserSchema), 14 | Schema.dropIfExists(ServerArticleSchema), 15 | ]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/app/middlewares/api_auth_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:backend/src/models.dart'; 2 | import 'package:pharaoh/pharaoh_next.dart'; 3 | 4 | import '../../src/services/auth_service.dart'; 5 | 6 | class ApiAuthMiddleware extends ClassMiddleware { 7 | final AuthService _authService; 8 | 9 | ApiAuthMiddleware(this._authService); 10 | 11 | @override 12 | handle(Request req, Response res, NextFunction next) async { 13 | final userId = _authService.validateRequest(req); 14 | if (userId == null) return next(res.unauthorized()); 15 | 16 | final user = await ServerUserQuery.findById(userId); 17 | if (user == null) return next(res.unauthorized()); 18 | 19 | return next(req..auth = user); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/app/middlewares/core_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:logging/logging.dart'; 2 | import 'package:pharaoh/pharaoh.dart'; 3 | import 'package:pharaoh/pharaoh_next.dart'; 4 | import 'package:shared/shared.dart'; 5 | 6 | class CoreMiddleware extends ClassMiddleware { 7 | late Middleware _webMdw; 8 | final Logger _logger; 9 | 10 | CoreMiddleware(this._logger) { 11 | // setup cookie parser 12 | final cookieConfig = app.instanceOf(); 13 | final cookieParserMdw = cookieParser(opts: cookieConfig); 14 | 15 | corsMiddleware(Request req, Response res, NextFunction next) { 16 | res = res 17 | ..header('Access-Control-Allow-Origin', appEnv.frontendURL.toString()) 18 | ..header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE') 19 | ..header('Access-Control-Allow-Headers', 'Content-Type, Authorization') 20 | ..header('Access-Control-Allow-Credentials', 'true') 21 | ..header('Access-Control-Max-Age', '3600'); 22 | 23 | if (req.method == HTTPMethod.OPTIONS) { 24 | return next(res.status(200).end()); 25 | } 26 | 27 | return next(res); 28 | } 29 | 30 | _webMdw = corsMiddleware.chain(cookieParserMdw).chain((req, res, next) { 31 | if (isTestMode) return next(); 32 | 33 | _logger.fine('${req.method.name}: ${req.path}'); 34 | next(); 35 | }); 36 | } 37 | 38 | @override 39 | Middleware get handler => _webMdw; 40 | } 41 | -------------------------------------------------------------------------------- /lib/app/middlewares/middlewares.dart: -------------------------------------------------------------------------------- 1 | export 'api_auth_middleware.dart'; 2 | export 'core_middleware.dart'; 3 | -------------------------------------------------------------------------------- /lib/app/providers/provide_blog_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:backend/src/services/services.dart'; 2 | import 'package:pharaoh/pharaoh_next.dart'; 3 | 4 | class BlogServiceProvider extends ServiceProvider { 5 | @override 6 | void register() { 7 | app.singleton(ArticleService()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/providers/provide_core.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:backend/src/services/services.dart'; 4 | import 'package:logging/logging.dart'; 5 | import 'package:pharaoh/pharaoh.dart'; 6 | import 'package:pharaoh/pharaoh_next.dart'; 7 | 8 | class CoreProvider extends ServiceProvider { 9 | @override 10 | void register() { 11 | final cookieConfig = CookieOpts( 12 | signed: true, 13 | secret: config.key, 14 | secure: !config.isDebug, 15 | maxAge: const Duration(hours: 1), 16 | ); 17 | 18 | app.singleton(cookieConfig); 19 | 20 | app.singleton(AuthService(config.key, config.url)); 21 | 22 | Logger.root 23 | ..level = Level.ALL 24 | ..onRecord.listen((record) { 25 | stdout.writeln('${record.level.name}: ${record.time}: ${record.message}'); 26 | }); 27 | 28 | app.singleton(Logger(config.environment)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/app/providers/provide_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:logging/logging.dart'; 2 | import 'package:pharaoh/pharaoh_next.dart'; 3 | import 'package:yaroorm/yaroorm.dart'; 4 | 5 | class DatabaseServiceProvider extends ServiceProvider { 6 | @override 7 | void boot() async { 8 | await DB.defaultDriver.connect(); 9 | 10 | final logger = app.instanceOf(); 11 | logger.info('Using ${DB.defaultConnection.info.name} database'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/app/providers/provide_routes.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:pharaoh/pharaoh_next.dart'; 4 | 5 | import '../routes/api.dart' as api; 6 | import '../routes/web.dart' as web; 7 | 8 | class RouteServiceProvider extends ServiceProvider { 9 | @override 10 | FutureOr boot() { 11 | app.useRoutes( 12 | () => [ 13 | /*|-------------------------------------------------------------------------- 14 | | API Routes 15 | |--------------------------------------------------------------------------*/ 16 | api.publicRoutes, 17 | api.authRoutes, 18 | 19 | /*|-------------------------------------------------------------------------- 20 | | Web Routes 21 | |--------------------------------------------------------------------------*/ 22 | Route.middleware('web').group('/', web.routes), 23 | ], 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/app/providers/providers.dart: -------------------------------------------------------------------------------- 1 | export 'provide_blog_services.dart'; 2 | export 'provide_core.dart'; 3 | export 'provide_database.dart'; 4 | export 'provide_routes.dart'; 5 | -------------------------------------------------------------------------------- /lib/app/routes/api.dart: -------------------------------------------------------------------------------- 1 | import 'package:backend/src/controllers/controllers.dart'; 2 | import 'package:pharaoh/pharaoh_next.dart'; 3 | 4 | final publicRoutes = Route.group('api', [ 5 | Route.post('/auth/login', (AuthController, #login)), 6 | Route.post('/auth/register', (AuthController, #register)), 7 | 8 | /// get articles and detail without auth 9 | Route.group('articles', [ 10 | Route.get('/', (ArticleController, #index)), 11 | Route.get('/', (ArticleController, #show)), 12 | ]), 13 | ]); 14 | 15 | final authRoutes = Route.middleware('api:auth').group('api', [ 16 | // Users 17 | Route.group('users', [ 18 | Route.get('/', (UserController, #index)), 19 | Route.get('/me', (UserController, #currentUser)), 20 | ]), 21 | 22 | // Articles 23 | Route.group('articles', [ 24 | Route.post('/', (ArticleController, #create)), 25 | Route.put('/', (ArticleController, #update)), 26 | Route.delete('/', (ArticleController, #delete)), 27 | ]), 28 | ]); 29 | -------------------------------------------------------------------------------- /lib/app/routes/web.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh_next.dart'; 2 | 3 | final routes = [ 4 | Route.route(HTTPMethod.GET, '/', (req, res) => res.ok('Hello World 🤘')), 5 | ]; 6 | -------------------------------------------------------------------------------- /lib/backend.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:backend/src/utils/config.dart'; 5 | import 'package:logging/logging.dart'; 6 | import 'package:pharaoh/pharaoh_next.dart'; 7 | 8 | import 'app/middlewares/core_middleware.dart'; 9 | import 'app/middlewares/api_auth_middleware.dart'; 10 | import 'app/providers/providers.dart'; 11 | 12 | export 'src/controllers/controllers.dart'; 13 | export 'src/dto/dto.dart'; 14 | 15 | final blogApp = App(appConfig); 16 | 17 | class App extends ApplicationFactory with AppInstance { 18 | App(super.appConfig); 19 | 20 | @override 21 | List get middlewares => [CoreMiddleware]; 22 | 23 | @override 24 | Map> get middlewareGroups => { 25 | 'web': [], 26 | 'api:auth': [ApiAuthMiddleware], 27 | }; 28 | 29 | @override 30 | List get providers => ServiceProvider.defaultProviders 31 | ..addAll([ 32 | CoreProvider, 33 | RouteServiceProvider, 34 | DatabaseServiceProvider, 35 | BlogServiceProvider, 36 | ]); 37 | 38 | @override 39 | FutureOr onApplicationException(PharaohError error, Request request, Response response) { 40 | final exception = error.exception; 41 | if (exception is RequestValidationError) { 42 | return response.json(exception.errorBody, statusCode: HttpStatus.badRequest); 43 | } 44 | 45 | app.instanceOf().severe(exception, null, error.trace); 46 | 47 | return super.onApplicationException(error, request, response); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/controllers/article_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'package:backend/src/dto/article_dto.dart'; 4 | import 'package:backend/src/models.dart'; 5 | import 'package:backend/src/services/services.dart'; 6 | import 'package:pharaoh/pharaoh_next.dart'; 7 | import 'package:shared/models.dart'; 8 | import 'package:yaroorm/yaroorm.dart'; 9 | 10 | import '../utils/utils.dart'; 11 | 12 | class ArticleController extends HTTPController { 13 | final ArticleService _articleService; 14 | 15 | ArticleController(this._articleService); 16 | 17 | Future index() async { 18 | final articles = await _articleService.getArticles(); 19 | return jsonResponse({'articles': articles.map((e) => e.toJson()).toList()}); 20 | } 21 | 22 | Future show(@param int articleId) async { 23 | final article = await _articleService.getArticle(articleId); 24 | if (article == null) return response.notFound(); 25 | return jsonResponse(_articleResponse(article)); 26 | } 27 | 28 | Future create(@body CreateArticleDTO data) async { 29 | final imageUrl = data.imageUrl ?? await Isolate.run(() => getRandomImage(data.title)); 30 | 31 | final article = await user.articles.insert(NewServerArticleForServerUser( 32 | title: data.title, 33 | description: data.description, 34 | imageUrl: Value(imageUrl), 35 | )); 36 | 37 | return response.json(_articleResponse(article)); 38 | } 39 | 40 | Future update(@param int articleId, @body CreateArticleDTO data) async { 41 | final article = await _articleService.updateArticle(user, articleId, data); 42 | if (article == null) return response.notFound(); 43 | return jsonResponse(_articleResponse(article)); 44 | } 45 | 46 | Future delete(@param int articleId) async { 47 | await _articleService.deleteArticle(user.id, articleId); 48 | return jsonResponse({'message': 'Article deleted'}); 49 | } 50 | 51 | Map _articleResponse(Article article) => {'article': article.toJson()}; 52 | 53 | ServerUser get user => request.auth as ServerUser; 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/controllers/auth_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:bcrypt/bcrypt.dart'; 4 | import 'package:pharaoh/pharaoh.dart'; 5 | import 'package:pharaoh/pharaoh_next.dart'; 6 | 7 | import '../dto/dto.dart'; 8 | import '../models.dart'; 9 | import '../services/services.dart'; 10 | 11 | class AuthController extends HTTPController { 12 | final AuthService _authService; 13 | 14 | AuthController(this._authService); 15 | 16 | Future login(@body LoginUserDTO data) async { 17 | final user = await ServerUserQuery.findByEmail(data.email); 18 | if (user == null) return invalidLogin; 19 | 20 | final match = BCrypt.checkpw(data.password, user.password); 21 | if (!match) return invalidLogin; 22 | 23 | final token = _authService.getAccessTokenForUser(user); 24 | final cookie = bakeCookie('auth', token, app.instanceOf()); 25 | 26 | return response.withCookie(cookie).json(_userResponse(user)); 27 | } 28 | 29 | Future register(@body CreateUserDTO data) async { 30 | final userExists = await ServerUserQuery.where((user) => user.email(data.email)).exists(); 31 | if (userExists) { 32 | return response.json( 33 | _makeError(['Email already taken']), 34 | statusCode: HttpStatus.badRequest, 35 | ); 36 | } 37 | 38 | final hashedPass = BCrypt.hashpw(data.password, BCrypt.gensalt()); 39 | final newUser = await ServerUserQuery.insert(NewServerUser( 40 | name: data.name, 41 | email: data.email, 42 | password: hashedPass, 43 | )); 44 | 45 | return response.json(_userResponse(newUser)); 46 | } 47 | 48 | Response get invalidLogin => response.unauthorized(data: _makeError(['Email or Password not valid'])); 49 | 50 | Map _userResponse(ServerUser user) => {'user': user.toJson()}; 51 | 52 | Map _makeError(List errors) => {'errors': errors}; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/controllers/controllers.dart: -------------------------------------------------------------------------------- 1 | export 'user_controller.dart'; 2 | export 'auth_controller.dart'; 3 | export 'article_controller.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/controllers/user_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:backend/src/models.dart'; 2 | import 'package:pharaoh/pharaoh_next.dart'; 3 | import 'package:shared/models.dart'; 4 | 5 | class UserController extends HTTPController { 6 | Future currentUser() async { 7 | final user = request.auth as User; 8 | return jsonResponse({'user': user.toJson()}); 9 | } 10 | 11 | Future index() async { 12 | final result = await ServerUserQuery.findMany(); 13 | return jsonResponse(result); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/dto/article_dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh_next.dart'; 2 | 3 | class CreateArticleDTO extends BaseDTO { 4 | @ezMinLength(5) 5 | String get title; 6 | 7 | @ezMinLength(10) 8 | String get description; 9 | 10 | @ezOptional(String) 11 | String? get imageUrl; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/dto/dto.dart: -------------------------------------------------------------------------------- 1 | export 'user_dto.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/dto/user_dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh_next.dart'; 2 | 3 | class CreateUserDTO extends BaseDTO { 4 | @ezMinLength(3) 5 | String get name; 6 | 7 | @ezEmail() 8 | String get email; 9 | 10 | @ezMinLength(8) 11 | String get password; 12 | } 13 | 14 | class LoginUserDTO extends BaseDTO { 15 | @ezEmail() 16 | String get email; 17 | 18 | String get password; 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:shared/models.dart'; 3 | import 'package:yaroorm/yaroorm.dart'; 4 | 5 | part 'models.g.dart'; 6 | 7 | @table 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | class ServerUser extends User with Entity { 10 | @JsonKey(defaultValue: '', includeToJson: false) 11 | final String password; 12 | 13 | ServerUser( 14 | @primaryKey super.id, 15 | super.name, 16 | super.email, { 17 | required this.password, 18 | @createdAtCol required super.createdAt, 19 | @updatedAtCol required super.updatedAt, 20 | }) { 21 | initialize(); 22 | } 23 | 24 | HasMany get articles => hasMany(#articles); 25 | 26 | @override 27 | Map toJson() => _$ServerUserToJson(this); 28 | 29 | factory ServerUser.fromJson(Map json) => _$ServerUserFromJson(json); 30 | } 31 | 32 | @table 33 | @JsonSerializable(fieldRename: FieldRename.snake) 34 | class ServerArticle extends Article with Entity { 35 | ServerArticle( 36 | @primaryKey super.id, 37 | super.title, 38 | @bindTo(ServerUser, onDelete: ForeignKeyAction.cascade) super.ownerId, 39 | super.description, { 40 | super.imageUrl, 41 | @createdAtCol required super.createdAt, 42 | @updatedAtCol required super.updatedAt, 43 | }) { 44 | initialize(); 45 | } 46 | 47 | BelongsTo get owner => belongsTo(#owner); 48 | 49 | @override 50 | Map toJson() => { 51 | ..._$ServerArticleToJson(this), 52 | if (owner.loaded) 'author': owner.value!.toJson(), 53 | }; 54 | 55 | factory ServerArticle.fromJson(Map json) => _$ServerArticleFromJson(json); 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/services/article_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:backend/src/models.dart'; 2 | import 'package:shared/models.dart'; 3 | import 'package:yaroorm/yaroorm.dart'; 4 | 5 | import 'package:backend/src/dto/article_dto.dart'; 6 | 7 | class ArticleService { 8 | Future> getArticles({int? ownerId}) async { 9 | final query = ServerArticleQuery.withRelations((article) => [article.owner]); 10 | if (ownerId == null) { 11 | return query.findMany(orderBy: [OrderServerArticleBy.createdAt(order: OrderDirection.desc)]); 12 | } 13 | 14 | return query 15 | .where((article) => article.ownerId(ownerId)) 16 | .findMany(orderBy: [OrderServerArticleBy.createdAt(order: OrderDirection.desc)]); 17 | } 18 | 19 | Future getArticle(int articleId) => 20 | ServerArticleQuery.withRelations((article) => [article.owner]).findById(articleId); 21 | 22 | Future updateArticle(User user, int articleId, CreateArticleDTO dto) async { 23 | final query = ServerArticleQuery.where((article) => and([ 24 | article.id(articleId), 25 | article.ownerId(user.id), 26 | ])); 27 | 28 | if (!(await query.exists())) return null; 29 | 30 | await query.update(UpdateServerArticle( 31 | title: Value.absentIfNull(dto.title), 32 | description: Value.absentIfNull(dto.description), 33 | imageUrl: Value.absentIfNull(dto.imageUrl), 34 | )); 35 | 36 | return query.findOne(); 37 | } 38 | 39 | Future deleteArticle(int userId, int articleId) { 40 | return ServerArticleQuery.where((article) => and([ 41 | article.id(articleId), 42 | article.ownerId(userId), 43 | ])).delete(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:pharaoh/pharaoh.dart'; 6 | import 'package:shared/models.dart'; 7 | 8 | class AuthService { 9 | final String jwtKey; 10 | final String issuer; 11 | 12 | const AuthService(this.jwtKey, this.issuer); 13 | 14 | JWTKey get _jwtKey => SecretKey(jwtKey); 15 | 16 | String getAccessTokenForUser(User user) { 17 | final jwt = JWT(user.toJson(), issuer: issuer, subject: user.id.toString(), jwtId: 'access-token'); 18 | return jwt.sign(_jwtKey, algorithm: JWTAlgorithm.HS256, expiresIn: Duration(hours: 1)); 19 | } 20 | 21 | /// this resolves the token from either signed cookies or 22 | /// request headers and returns the User ID 23 | int? validateRequest(Request request) { 24 | String? authToken; 25 | if (request.signedCookies.isNotEmpty) { 26 | authToken = request.signedCookies.firstWhereOrNull((e) => e.name == 'auth')?.value; 27 | } else if (request.headers.containsKey(HttpHeaders.authorizationHeader)) { 28 | authToken = request.headers[HttpHeaders.authorizationHeader]?.toString().split(' ').last; 29 | } 30 | 31 | if (authToken == null) return null; 32 | 33 | try { 34 | final jwt = JWT.verify(authToken, _jwtKey); 35 | if (jwt.jwtId != 'access-token') return null; 36 | return int.tryParse('${jwt.subject}'); 37 | } catch (e) { 38 | return null; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/services/services.dart: -------------------------------------------------------------------------------- 1 | export 'auth_service.dart'; 2 | export 'article_service.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/utils/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart' as path; 2 | import 'package:pharaoh/pharaoh_next.dart'; 3 | import 'package:shared/shared.dart'; 4 | import 'package:uuid/v4.dart'; 5 | import 'package:yaroorm/yaroorm.dart'; 6 | 7 | final appConfig = AppConfig( 8 | name: 'Dart Blog', 9 | environment: env('APP_ENV', appEnv.name), 10 | isDebug: appEnv == AppEnvironment.local, 11 | url: env('APP_URL', 'http://localhost'), 12 | port: env('PORT', 80), 13 | key: env('APP_KEY', UuidV4().generate()), 14 | ); 15 | 16 | extension on AppEnvironment { 17 | DatabaseConnection get dbCon => switch (this) { 18 | AppEnvironment.prod => DatabaseConnection( 19 | AppEnvironment.prod.name, 20 | DatabaseDriverType.pgsql, 21 | port: env('DB_PORT', 6543), 22 | host: env('DB_HOST', ''), 23 | username: env('DB_USERNAME', ''), 24 | password: env('DB_PASSWORD', ''), 25 | database: env('DB_DATABASE', ''), 26 | secure: true, 27 | ), 28 | _ => DatabaseConnection( 29 | AppEnvironment.local.name, 30 | DatabaseDriverType.sqlite, 31 | database: path.absolute('database', 'db.sqlite'), 32 | ), 33 | }; 34 | } 35 | 36 | @DB.useConfig 37 | final config = YaroormConfig( 38 | appEnv.name, 39 | connections: AppEnvironment.values.map((e) => e.dbCon).toList(), 40 | ); 41 | -------------------------------------------------------------------------------- /lib/src/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:http/http.dart' as http; 5 | import 'package:pharaoh/pharaoh_next.dart'; 6 | import 'package:shared/shared.dart'; 7 | 8 | Future getRandomImage(String searchText) async { 9 | if (isTestMode) return defaultArticleImage; 10 | 11 | String? resultingImageUrl; 12 | 13 | try { 14 | final response = await http.get( 15 | Uri.parse('https://api.pexels.com/v1/search?query=$searchText&per_page=1'), 16 | headers: {HttpHeaders.authorizationHeader: env('PEXELS_API_KEY', '')}, 17 | ).timeout(const Duration(seconds: 5)); 18 | final result = jsonDecode(response.body) as Map; 19 | resultingImageUrl = result['photos'][0]['src']['medium']; 20 | } catch (error, trace) { 21 | stderr.writeln('An error occurred while getting image for $searchText'); 22 | stderr.writeln('Error $error'); 23 | stderr.writeln('Trace $trace'); 24 | } finally { 25 | resultingImageUrl ??= defaultArticleImage; 26 | } 27 | 28 | return resultingImageUrl; 29 | } 30 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: 'dart_blog' 2 | 3 | packages: 4 | - 'packages/shared' 5 | - 'packages/frontend' 6 | - '.' -------------------------------------------------------------------------------- /melos_backend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /melos_dart_blog.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/frontend/.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.24.3", 3 | "flavors": {} 4 | } 5 | -------------------------------------------------------------------------------- /packages/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | .vscode/ 13 | 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | **.g.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | # FVM Version Cache 48 | .fvm/ -------------------------------------------------------------------------------- /packages/frontend/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 18 | - platform: android 19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 21 | - platform: ios 22 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 23 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 24 | - platform: linux 25 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 26 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 27 | - platform: macos 28 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 29 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 30 | - platform: web 31 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 32 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 33 | - platform: windows 34 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 35 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /packages/frontend/README.md: -------------------------------------------------------------------------------- 1 | # web 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /packages/frontend/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | 18 | analyzer: 19 | exclude: 20 | - "**.g.dart" -------------------------------------------------------------------------------- /packages/frontend/lib/auth/auth.dart: -------------------------------------------------------------------------------- 1 | export 'pages/login_page.dart'; 2 | export 'pages/register_page.dart'; 3 | export 'auth_header.dart'; 4 | -------------------------------------------------------------------------------- /packages/frontend/lib/auth/auth_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/data/providers/auth_provider.dart'; 3 | import 'package:frontend/main.dart'; 4 | import 'package:frontend/utils/provider.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:shared/models.dart'; 7 | 8 | class AuthHeaderOptions extends StatelessWidget { 9 | const AuthHeaderOptions({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final auth = context.read(); 14 | const spacing = SizedBox(width: 24); 15 | 16 | return StreamBuilder>( 17 | stream: auth.stream, 18 | initialData: auth.lastEvent, 19 | builder: (context, snapshot) { 20 | final user = auth.user; 21 | final isLoading = snapshot.data?.state == ProviderState.loading; 22 | 23 | return Row( 24 | mainAxisSize: MainAxisSize.min, 25 | children: [ 26 | if (!isLoading && user == null) ...[ 27 | Button(child: const Text('Login '), onPressed: () => router.push('/login')), 28 | spacing, 29 | Button(child: const Text('Register'), onPressed: () => router.push('/register')), 30 | ], 31 | if (user != null) ...[ 32 | Text('Welcome, ${user.name.split(' ').first}'), 33 | spacing, 34 | Button( 35 | child: const Text('Logout'), 36 | onPressed: () { 37 | auth.logout(); 38 | router.pushReplacement('/'); 39 | }, 40 | ), 41 | ], 42 | ], 43 | ); 44 | }, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/frontend/lib/auth/pages/auth_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/data/providers/auth_provider.dart'; 3 | import 'package:frontend/utils/misc.dart'; 4 | import 'package:frontend/utils/provider.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:shared/models.dart'; 7 | 8 | class BaseAuthLayout extends StatefulWidget { 9 | final Widget Function(AuthProvider auth, BaseAuthLayoutState layout) child; 10 | 11 | const BaseAuthLayout({super.key, required this.child}); 12 | 13 | @override 14 | State createState() => BaseAuthLayoutState(); 15 | } 16 | 17 | class BaseAuthLayoutState extends State { 18 | bool _showingLoading = false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final authProv = context.read(); 23 | return ScaffoldPage( 24 | padding: EdgeInsets.zero, 25 | content: Stack( 26 | children: [ 27 | Container(color: Colors.grey), 28 | Center( 29 | child: SizedBox( 30 | width: 320, 31 | child: Card( 32 | padding: EdgeInsets.zero, 33 | child: Column( 34 | mainAxisSize: MainAxisSize.min, 35 | children: [ 36 | if (_showingLoading) const SizedBox(width: double.maxFinite, child: ProgressBar()), 37 | Container( 38 | padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), 39 | child: widget.child(authProv, this), 40 | ), 41 | ], 42 | ), 43 | ), 44 | ), 45 | ), 46 | ], 47 | ), 48 | ); 49 | } 50 | 51 | void setLoading(bool show) => setState(() => _showingLoading = show); 52 | 53 | void handleErrors(ProviderEvent event) { 54 | if (event.state != ProviderState.error) return; 55 | showError(context, event.errorMessage!); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/frontend/lib/auth/pages/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/main.dart'; 3 | 4 | import 'auth_layout.dart'; 5 | 6 | const _spacing = SizedBox(height: 18); 7 | 8 | class LoginPage extends StatefulWidget { 9 | final String? returnUrl; 10 | 11 | const LoginPage({super.key, this.returnUrl}); 12 | 13 | @override 14 | State createState() => _LoginPageState(); 15 | } 16 | 17 | class _LoginPageState extends State { 18 | String? email; 19 | String? password; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final themeData = FluentTheme.of(context); 24 | 25 | return BaseAuthLayout( 26 | child: (auth, layout) { 27 | loginAction(String email, String password) async { 28 | layout.setLoading(true); 29 | await auth.login(email, password); 30 | 31 | final lastEvent = auth.lastEvent!; 32 | if (lastEvent.data != null) { 33 | return router.pushReplacement(widget.returnUrl ?? '/'); 34 | } else { 35 | router.pushReplacement('/login'); 36 | } 37 | 38 | layout 39 | ..setLoading(false) 40 | ..handleErrors(lastEvent); 41 | } 42 | 43 | return Column( 44 | mainAxisSize: MainAxisSize.min, 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | InfoLabel( 48 | label: 'Email', 49 | child: TextBox(onChanged: (value) => setState(() => email = value)), 50 | ), 51 | _spacing, 52 | InfoLabel( 53 | label: 'Password', 54 | child: PasswordBox(onChanged: (value) => setState(() => password = value)), 55 | ), 56 | _spacing, 57 | GestureDetector( 58 | onTap: () => router.push('/register'), 59 | child: Text.rich( 60 | TextSpan( 61 | text: 'No account? ', 62 | children: [ 63 | TextSpan( 64 | text: 'Create one!', 65 | style: themeData.typography.body?.apply(color: themeData.accentColor.dark)), 66 | ], 67 | ), 68 | ), 69 | ), 70 | const SizedBox(height: 32), 71 | Row( 72 | children: [ 73 | const Expanded(child: SizedBox.shrink()), 74 | FilledButton( 75 | style: ButtonStyle( 76 | shape: WidgetStateProperty.all(const RoundedRectangleBorder(borderRadius: BorderRadius.zero)), 77 | ), 78 | onPressed: [email, password].contains(null) ? null : () => loginAction(email!, password!), 79 | child: const Text('Sign in'), 80 | ) 81 | ], 82 | ) 83 | ], 84 | ); 85 | }, 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/frontend/lib/auth/pages/register_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/main.dart'; 3 | 4 | import 'auth_layout.dart'; 5 | 6 | const _spacing = SizedBox(height: 24); 7 | 8 | class RegisterPage extends StatefulWidget { 9 | const RegisterPage({super.key}); 10 | 11 | @override 12 | State createState() => _RegisterPageState(); 13 | } 14 | 15 | class _RegisterPageState extends State { 16 | String? name; 17 | String? email; 18 | String? password; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return BaseAuthLayout( 23 | child: (auth, layout) { 24 | registerAction(String name, String email, String password) async { 25 | layout.setLoading(true); 26 | final success = await auth.register(name, email, password); 27 | router.pushReplacement(success ? '/login' : '/register'); 28 | 29 | layout 30 | ..setLoading(false) 31 | ..handleErrors(auth.lastEvent!); 32 | } 33 | 34 | return Column( 35 | mainAxisSize: MainAxisSize.min, 36 | children: [ 37 | InfoLabel( 38 | label: 'Display Name', 39 | child: TextBox( 40 | keyboardType: TextInputType.name, onChanged: (value) => setState(() => name = value.trim()))), 41 | _spacing, 42 | InfoLabel( 43 | label: 'Email', 44 | child: TextBox( 45 | keyboardType: TextInputType.emailAddress, 46 | onChanged: (value) => setState(() => email = value.trim()))), 47 | _spacing, 48 | InfoLabel(label: 'Password', child: PasswordBox(onChanged: (value) => setState(() => password = value))), 49 | const SizedBox(height: 28), 50 | Row( 51 | children: [ 52 | const Expanded(child: SizedBox.shrink()), 53 | FilledButton( 54 | style: ButtonStyle( 55 | shape: WidgetStateProperty.all(const RoundedRectangleBorder(borderRadius: BorderRadius.zero)), 56 | ), 57 | onPressed: 58 | [name, email, password].contains(null) ? null : () => registerAction(name!, email!, password!), 59 | child: const Text('Register'), 60 | ) 61 | ], 62 | ) 63 | ], 64 | ); 65 | }, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/frontend/lib/blog/blog.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/data/providers/article_provider.dart'; 3 | import 'package:frontend/data/providers/auth_provider.dart'; 4 | import 'package:frontend/utils/misc.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | import 'widgets/article_items.dart'; 8 | 9 | class BlogPage extends StatefulWidget { 10 | const BlogPage({super.key}); 11 | 12 | @override 13 | State createState() => _BlogPageState(); 14 | } 15 | 16 | class _BlogPageState extends State { 17 | late AuthProvider _authProvider; 18 | late ArticleProvider _articleProvider; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | 24 | _authProvider = context.read(); 25 | _articleProvider = context.read(); 26 | 27 | _fetchData(); 28 | } 29 | 30 | void _fetchData() async { 31 | await Future.wait([ 32 | _authProvider.getUser(), 33 | _articleProvider.fetchArticles(), 34 | ]); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return WebConstrainedLayout( 40 | child: ScaffoldPage.scrollable(children: const [BlogArticlesWidget()]), 41 | ); 42 | } 43 | } 44 | 45 | class WebConstrainedLayout extends StatelessWidget { 46 | final Widget child; 47 | const WebConstrainedLayout({super.key, required this.child}); 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | final borderSide = BorderSide(color: blogColor.withOpacity(0.1)); 52 | 53 | return Container( 54 | alignment: Alignment.topCenter, 55 | child: Container( 56 | constraints: const BoxConstraints(maxWidth: 1300), 57 | decoration: BoxDecoration(border: Border(left: borderSide, right: borderSide, top: borderSide)), 58 | margin: const EdgeInsets.all(24), 59 | child: child, 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/frontend/lib/blog/blog_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/data/providers/article_provider.dart'; 3 | import 'package:frontend/data/providers/auth_provider.dart'; 4 | import 'package:frontend/main.dart'; 5 | import 'package:frontend/utils/misc.dart'; 6 | import 'package:markdown_widget/markdown_widget.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:shared/shared.dart'; 9 | import 'package:timeago/timeago.dart' as timeago; 10 | 11 | import 'widgets/article_base_layout.dart'; 12 | 13 | class BlogDetail extends StatelessWidget { 14 | final String articleId; 15 | 16 | const BlogDetail(this.articleId, {super.key}); 17 | 18 | @override 19 | @override 20 | Widget build(BuildContext context) { 21 | return ArticleBaseLayout( 22 | articleId: int.tryParse(articleId), 23 | child: (detail, layout) { 24 | final currentUser = context.read().user; 25 | final articleProv = context.read(); 26 | 27 | final article = detail.article; 28 | final owner = detail.owner; 29 | 30 | if (article == null) { 31 | if (detail.isLoading) return loadingView(); 32 | if (detail.hasError) return errorView(message: detail.errorMessage); 33 | return const SizedBox.shrink(); 34 | } 35 | 36 | const footerStyle = TextStyle(fontSize: 12, fontWeight: FontWeight.w300); 37 | final isPostOwner = currentUser != null && owner != null && currentUser.id == owner.id; 38 | final imageUri = article.imageUrl ?? defaultArticleImage; 39 | 40 | final imageHost = Uri.tryParse(imageUri)?.host; 41 | const spacing = SizedBox(height: 24); 42 | 43 | return Column( 44 | children: [ 45 | PageHeader( 46 | title: Text(article.title, style: const TextStyle(color: Colors.black)), 47 | commandBar: CommandBar( 48 | crossAxisAlignment: CrossAxisAlignment.center, 49 | mainAxisAlignment: MainAxisAlignment.end, 50 | primaryItems: [ 51 | if (isPostOwner) 52 | CommandBarButton( 53 | icon: const Icon(FluentIcons.edit), 54 | label: const Text('Edit'), 55 | onPressed: () => router.pushReplacement('/posts/${article.id}/edit'), 56 | ), 57 | if (isPostOwner) 58 | CommandBarButton( 59 | icon: Icon(FluentIcons.delete, color: Colors.red), 60 | label: Text( 61 | 'Delete', 62 | style: TextStyle(color: Colors.red), 63 | ), 64 | onPressed: () { 65 | showDialog( 66 | context: context, 67 | builder: (context) { 68 | return ContentDialog( 69 | title: const Text('Delete Blog permanently?'), 70 | content: const Text( 71 | 'If you delete this file, you won\'t be able to recover it. Do you want to delete it?', 72 | ), 73 | actions: [ 74 | FilledButton( 75 | style: ButtonStyle( 76 | backgroundColor: WidgetStateProperty.all(Colors.red), 77 | shape: WidgetStateProperty.all( 78 | const RoundedRectangleBorder(borderRadius: BorderRadius.zero), 79 | )), 80 | onPressed: () async { 81 | await articleProv 82 | .deleteArticle(int.tryParse(articleId)!) 83 | .then((value) => router.pushReplacement('/')); 84 | }, 85 | child: const Text("Delete"), 86 | ), 87 | FilledButton( 88 | style: ButtonStyle( 89 | shape: WidgetStateProperty.all( 90 | const RoundedRectangleBorder(borderRadius: BorderRadius.zero))), 91 | onPressed: () => router.pop(context), 92 | child: const Text("Cancel"), 93 | ), 94 | ], 95 | ); 96 | }); 97 | }, 98 | ), 99 | ], 100 | ), 101 | padding: 0, 102 | ), 103 | Divider( 104 | style: DividerThemeData( 105 | thickness: 0.2, 106 | decoration: BoxDecoration(border: Border(top: BorderSide(color: Colors.grey.withOpacity(0.05)))), 107 | ), 108 | ), 109 | spacing, 110 | Row( 111 | crossAxisAlignment: CrossAxisAlignment.start, 112 | children: [ 113 | SizedBox( 114 | width: 400, 115 | height: 250, 116 | child: Column( 117 | mainAxisSize: MainAxisSize.min, 118 | children: [ 119 | Expanded(child: imageView(imageUri)), 120 | if (imageHost != null) ...[ 121 | const SizedBox(height: 8), 122 | Text(imageHost, style: const TextStyle(fontWeight: FontWeight.w300, fontSize: 12)), 123 | ] 124 | ], 125 | ), 126 | ), 127 | const SizedBox(width: 40), 128 | Expanded( 129 | child: Container( 130 | constraints: const BoxConstraints(minHeight: 350), 131 | alignment: Alignment.topRight, 132 | child: MarkdownWidget(data: article.description, shrinkWrap: true), 133 | ), 134 | ), 135 | ], 136 | ), 137 | Container( 138 | height: 40, 139 | decoration: BoxDecoration( 140 | border: Border(top: BorderSide(color: Colors.grey.withOpacity(0.5))), 141 | ), 142 | child: Row( 143 | children: [ 144 | Text('Last Updated: ${timeago.format(article.updatedAt)}', style: footerStyle), 145 | const Spacer(), 146 | if (owner != null) Text('Author: ${owner.name}', style: footerStyle) 147 | ], 148 | ), 149 | ) 150 | ], 151 | ); 152 | }, 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /packages/frontend/lib/blog/widgets/add_article_card.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | import 'package:fluent_ui/fluent_ui.dart'; 3 | import 'package:frontend/data/providers/auth_provider.dart'; 4 | import 'package:frontend/main.dart'; 5 | import 'package:frontend/utils/misc.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class AddArticleCard extends StatelessWidget { 9 | const AddArticleCard({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return const DottedBorder(); 14 | } 15 | } 16 | 17 | class DottedBorder extends StatelessWidget { 18 | final Color color; 19 | final double strokeWidth; 20 | final double gap; 21 | 22 | const DottedBorder({super.key, this.color = Colors.black, this.strokeWidth = 1.5, this.gap = 5.0}); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final typography = FluentTheme.of(context).typography; 27 | final user = context.read().user; 28 | 29 | return Padding( 30 | padding: EdgeInsets.all(strokeWidth / 2), 31 | child: GestureDetector( 32 | onTap: () { 33 | if (user == null) { 34 | router.pushReplacement("/login", extra: {'returnUrl': '/posts/new'}); 35 | return; 36 | } 37 | 38 | router.push('/posts/new'); 39 | }, 40 | child: Container( 41 | alignment: Alignment.center, 42 | width: 300, 43 | height: 250, 44 | child: CustomPaint( 45 | painter: DashRectPainter(color: color, strokeWidth: strokeWidth, gap: gap), 46 | child: SizedBox( 47 | width: 200, 48 | height: 200, 49 | child: Card( 50 | borderRadius: const BorderRadius.all(Radius.circular(5)), 51 | child: Column( 52 | crossAxisAlignment: CrossAxisAlignment.center, 53 | mainAxisAlignment: MainAxisAlignment.center, 54 | children: [ 55 | const Icon(FluentIcons.page_add, size: 34), 56 | const SizedBox(height: 16), 57 | Text( 58 | "New Post +", 59 | style: typography.bodyStrong!.copyWith(color: blogColor), 60 | maxLines: 1, 61 | overflow: TextOverflow.ellipsis, 62 | ), 63 | ], 64 | ), 65 | ), 66 | )), 67 | ), 68 | ), 69 | ); 70 | } 71 | } 72 | 73 | class DashRectPainter extends CustomPainter { 74 | double strokeWidth; 75 | Color color; 76 | double gap; 77 | 78 | DashRectPainter({this.strokeWidth = 5.0, this.color = Colors.black, this.gap = 5.0}); 79 | 80 | @override 81 | void paint(Canvas canvas, Size size) { 82 | Paint dashedPaint = Paint() 83 | ..color = color 84 | ..strokeWidth = strokeWidth 85 | ..style = PaintingStyle.stroke; 86 | 87 | double x = size.width; 88 | double y = size.height; 89 | 90 | Path topPath = getDashedPath( 91 | a: const math.Point(0, 0), 92 | b: math.Point(x, 0), 93 | gap: gap, 94 | ); 95 | 96 | Path rightPath = getDashedPath( 97 | a: math.Point(x, 0), 98 | b: math.Point(x, y), 99 | gap: gap, 100 | ); 101 | 102 | Path bottomPath = getDashedPath( 103 | a: math.Point(0, y), 104 | b: math.Point(x, y), 105 | gap: gap, 106 | ); 107 | 108 | Path leftPath = getDashedPath( 109 | a: const math.Point(0, 0), 110 | b: math.Point(0.001, y), 111 | gap: gap, 112 | ); 113 | 114 | canvas.drawPath(topPath, dashedPaint); 115 | canvas.drawPath(rightPath, dashedPaint); 116 | canvas.drawPath(bottomPath, dashedPaint); 117 | canvas.drawPath(leftPath, dashedPaint); 118 | } 119 | 120 | Path getDashedPath({ 121 | required math.Point a, 122 | required math.Point b, 123 | required gap, 124 | }) { 125 | Size size = Size(b.x - a.x, b.y - a.y); 126 | Path path = Path(); 127 | path.moveTo(a.x, a.y); 128 | bool shouldDraw = true; 129 | math.Point currentPoint = math.Point(a.x, a.y); 130 | 131 | num radians = math.atan(size.height / size.width); 132 | 133 | num dx = math.cos(radians) * gap < 0 ? math.cos(radians) * gap * -1 : math.cos(radians) * gap; 134 | 135 | num dy = math.sin(radians) * gap < 0 ? math.sin(radians) * gap * -1 : math.sin(radians) * gap; 136 | 137 | while (currentPoint.x <= b.x && currentPoint.y <= b.y) { 138 | shouldDraw 139 | ? path.lineTo(double.parse(currentPoint.x.toString()), double.parse(currentPoint.y.toString())) 140 | : path.moveTo(double.parse(currentPoint.x.toString()), double.parse(currentPoint.y.toString())); 141 | shouldDraw = !shouldDraw; 142 | currentPoint = math.Point( 143 | currentPoint.x + dx, 144 | currentPoint.y + dy, 145 | ); 146 | } 147 | return path; 148 | } 149 | 150 | @override 151 | bool shouldRepaint(CustomPainter oldDelegate) { 152 | return true; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/frontend/lib/blog/widgets/article_base_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/blog/blog.dart'; 3 | import 'package:frontend/data/api_service.dart'; 4 | import 'package:frontend/main.dart'; 5 | 6 | import 'package:frontend/utils/provider.dart'; 7 | import 'package:shared/models.dart'; 8 | 9 | import '../../utils/misc.dart'; 10 | 11 | class ArticleBaseLayout extends StatefulWidget { 12 | final int? articleId; 13 | 14 | final Widget Function(ArticleDetailLoader detailProv, ArticleBaseLayoutState layout) child; 15 | 16 | const ArticleBaseLayout({this.articleId, super.key, required this.child}); 17 | 18 | @override 19 | State createState() => ArticleBaseLayoutState(); 20 | } 21 | 22 | class ArticleBaseLayoutState extends State { 23 | final _detailProvider = ArticleDetailLoader(); 24 | 25 | bool _showingLoading = false; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | final id = widget.articleId; 32 | if (id != null) _detailProvider.fetchArticle(id); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return WebConstrainedLayout( 38 | child: StreamBuilder>( 39 | stream: _detailProvider.stream, 40 | initialData: _detailProvider.lastEvent, 41 | builder: (_, __) => ScaffoldPage.scrollable( 42 | children: [ 43 | if (_showingLoading) const SizedBox(width: double.maxFinite, child: ProgressBar()), 44 | Padding( 45 | padding: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 30), 46 | child: widget.child(_detailProvider, this), 47 | ), 48 | ], 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | super.dispose(); 57 | _detailProvider.dispose(); 58 | } 59 | 60 | void setLoading(bool show) => setState(() => _showingLoading = show); 61 | 62 | void handleErrors(ProviderEvent
event) { 63 | if (event.state != ProviderState.error) return; 64 | showError(context, event.errorMessage!); 65 | } 66 | } 67 | 68 | class ArticleDetailLoader extends BaseProvider { 69 | ApiService get apiSvc => getIt.get(); 70 | 71 | Article? get article => lastEvent?.data?.article; 72 | 73 | User? get owner => lastEvent?.data?.author; 74 | 75 | Future fetchArticle(int articleId) async { 76 | final result = await safeRun(() => apiSvc.getArticle(articleId)); 77 | if (result == null) return; 78 | 79 | addEvent(ProviderEvent.success(data: result)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/frontend/lib/blog/widgets/article_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/main.dart'; 3 | import 'package:frontend/utils/misc.dart'; 4 | import 'package:shared/models.dart'; 5 | 6 | class ArticleCard extends StatelessWidget { 7 | final Article article; 8 | 9 | const ArticleCard(this.article, {super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final typography = FluentTheme.of(context).typography; 14 | return GestureDetector( 15 | onTap: () => router.push('/posts/${article.id}'), 16 | child: SizedBox( 17 | width: 300, 18 | height: 250, 19 | child: Card( 20 | borderColor: Colors.grey.withOpacity(0.3), 21 | borderRadius: BorderRadius.zero, 22 | padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24), 23 | child: Column( 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | Expanded( 27 | child: article.imageUrl != null 28 | ? imageView(article.imageUrl!) 29 | : Container(color: blogColor.withOpacity(0.1)), 30 | ), 31 | const SizedBox(height: 16), 32 | Text( 33 | article.title, 34 | style: typography.bodyStrong!.copyWith(color: blogColor), 35 | maxLines: 1, 36 | overflow: TextOverflow.ellipsis, 37 | ), 38 | const SizedBox(height: 8), 39 | Text( 40 | article.description, 41 | style: typography.bodyLarge!.copyWith(color: blogColor, fontSize: 12), 42 | maxLines: 2, 43 | overflow: TextOverflow.ellipsis, 44 | ) 45 | ], 46 | ), 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/frontend/lib/blog/widgets/article_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:appflowy_editor/appflowy_editor.dart'; 2 | import 'package:fluent_ui/fluent_ui.dart'; 3 | import 'package:frontend/blog/widgets/article_base_layout.dart'; 4 | import 'package:frontend/data/providers/article_provider.dart'; 5 | import 'package:frontend/main.dart'; 6 | import 'package:frontend/utils/misc.dart'; 7 | import 'package:frontend/utils/provider.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | const _spacing = SizedBox(height: 24); 11 | 12 | class ArticleFormView extends StatefulWidget { 13 | final String? articleId; 14 | const ArticleFormView({super.key, this.articleId}); 15 | 16 | @override 17 | State createState() => _ArticleFormViewState(); 18 | } 19 | 20 | class _ArticleFormViewState extends State { 21 | final _titleCtrl = TextEditingController(); 22 | final _imageUrlCtrl = TextEditingController(); 23 | 24 | bool hasSetDefaults = false; 25 | 26 | int? articleId; 27 | 28 | EditorState? editorState; 29 | 30 | bool get isValidPost => _titleCtrl.text.trim().isNotEmpty && editorState != null && !editorState!.document.isEmpty; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | 36 | if (widget.articleId != null) { 37 | articleId = int.tryParse(widget.articleId!); 38 | if (articleId == null) router.pop(); 39 | } 40 | if (widget.articleId == null) editorState = EditorState.blank(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final typography = FluentTheme.of(context).typography; 46 | 47 | return ArticleBaseLayout( 48 | articleId: articleId, 49 | child: (detailProv, layout) { 50 | final articleProv = context.read(); 51 | final maybeArticle = detailProv.article; 52 | 53 | if (articleId != null && maybeArticle == null) { 54 | if (detailProv.isLoading) return loadingView(); 55 | if (detailProv.hasError) { 56 | layout.handleErrors(ProviderEvent.error(errorMessage: detailProv.errorMessage!)); 57 | router.pop(); 58 | return const SizedBox.shrink(); 59 | } 60 | } 61 | 62 | if (maybeArticle != null && !hasSetDefaults) { 63 | _titleCtrl.text = maybeArticle.title; 64 | editorState = EditorState(document: markdownToDocument(maybeArticle.description)); 65 | final imageUrl = maybeArticle.imageUrl; 66 | if (imageUrl != null) _imageUrlCtrl.text = imageUrl; 67 | } 68 | 69 | createOrUpdateAction() async { 70 | if (!isValidPost) { 71 | layout.handleErrors(const ProviderEvent.error(errorMessage: 'Post is not valid')); 72 | return; 73 | } 74 | 75 | final title = _titleCtrl.text.trim(); 76 | final description = documentToMarkdown(editorState!.document); 77 | final imageUrl = Uri.tryParse(_imageUrlCtrl.text)?.toString(); 78 | 79 | layout.setLoading(true); 80 | 81 | if (maybeArticle != null) { 82 | await articleProv.updateArticle(maybeArticle.id, title, description, imageUrl); 83 | } else if (widget.articleId == null) { 84 | await articleProv.addArticle(title, description, imageUrl); 85 | } 86 | 87 | layout.setLoading(false); 88 | 89 | if (articleProv.hasError) { 90 | layout.handleErrors(ProviderEvent.error(errorMessage: articleProv.errorMessage!)); 91 | return; 92 | } 93 | 94 | router.pushReplacement('/'); 95 | } 96 | 97 | if (editorState == null) return loadingView(); 98 | 99 | return Column( 100 | mainAxisSize: MainAxisSize.min, 101 | crossAxisAlignment: CrossAxisAlignment.start, 102 | children: [ 103 | PageHeader( 104 | title: Text(maybeArticle != null ? 'Update Post' : 'New Post'), 105 | padding: 0, 106 | ), 107 | _spacing, 108 | InfoLabel( 109 | label: 'Title', 110 | labelStyle: const TextStyle(fontWeight: FontWeight.w300), 111 | child: TextBox( 112 | controller: _titleCtrl, 113 | keyboardType: TextInputType.text, 114 | autofocus: true, 115 | placeholder: 'Post Title', 116 | style: typography.bodyLarge!.copyWith(color: blogColor), 117 | ), 118 | ), 119 | const SizedBox(height: 32), 120 | InfoLabel(label: 'Write your post'), 121 | const SizedBox(height: 8), 122 | Container( 123 | constraints: const BoxConstraints(maxHeight: 400), 124 | width: double.maxFinite, 125 | child: Card( 126 | borderColor: blogColor.withOpacity(0.1), 127 | borderRadius: BorderRadius.zero, 128 | child: AppFlowyEditor( 129 | shrinkWrap: true, 130 | editorStyle: EditorStyle.desktop( 131 | padding: EdgeInsets.zero, selectionColor: Colors.grey.withOpacity(0.5), cursorColor: blogColor), 132 | editorState: editorState!, 133 | ), 134 | ), 135 | ), 136 | _spacing, 137 | InfoLabel( 138 | label: 'Image Url (Optional)', 139 | labelStyle: const TextStyle(fontWeight: FontWeight.w300), 140 | child: TextBox(controller: _imageUrlCtrl, keyboardType: TextInputType.url), 141 | ), 142 | const SizedBox(height: 28), 143 | Row( 144 | children: [ 145 | const Expanded(child: SizedBox.shrink()), 146 | FilledButton( 147 | style: ButtonStyle( 148 | shape: WidgetStateProperty.all(const RoundedRectangleBorder(borderRadius: BorderRadius.zero))), 149 | onPressed: createOrUpdateAction, 150 | child: Text(widget.articleId == null ? 'Add Post' : 'Update Post'), 151 | ) 152 | ], 153 | ) 154 | ], 155 | ); 156 | }, 157 | ); 158 | } 159 | 160 | @override 161 | void dispose() { 162 | super.dispose(); 163 | _titleCtrl.dispose(); 164 | _imageUrlCtrl.dispose(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /packages/frontend/lib/blog/widgets/article_items.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:frontend/blog/widgets/add_article_card.dart'; 3 | import 'package:frontend/data/providers/article_provider.dart'; 4 | import 'package:frontend/utils/misc.dart'; 5 | import 'package:frontend/utils/provider.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:shared/models.dart'; 8 | 9 | import 'article_card.dart'; 10 | 11 | class BlogArticlesWidget extends StatelessWidget { 12 | const BlogArticlesWidget({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | const spacing = 16.0; 17 | final articleProv = context.read(); 18 | 19 | return StreamBuilder>>( 20 | stream: articleProv.stream, 21 | initialData: articleProv.lastEvent, 22 | builder: (context, snapshot) { 23 | final articles = snapshot.data?.data ?? []; 24 | final state = snapshot.data?.state; 25 | final loading = state == ProviderState.loading; 26 | 27 | if (articles.isEmpty) { 28 | if (loading) return loadingView(); 29 | if (state == ProviderState.error) return errorView(); 30 | } 31 | 32 | return Wrap( 33 | runSpacing: spacing, 34 | spacing: spacing, 35 | alignment: WrapAlignment.start, 36 | children: [const AddArticleCard(), ...articles.map((e) => ArticleCard(e))], 37 | ); 38 | }, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/frontend/lib/data/api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | // ignore: avoid_web_libraries_in_flutter 5 | import 'dart:html' as html; 6 | 7 | import 'package:http/http.dart' show Response; 8 | import 'package:http/browser_client.dart'; 9 | import 'package:shared/models.dart'; 10 | 11 | typedef ArticleWithAuthor = ({Article article, User author}); 12 | 13 | class ApiException extends HttpException { 14 | final Iterable errors; 15 | ApiException(this.errors) : super(errors.join('\n')); 16 | } 17 | 18 | typedef HttpResponseCb = Future Function(); 19 | 20 | class ApiService { 21 | final Uri baseUrl; 22 | final BrowserClient client; 23 | 24 | ApiService(this.baseUrl) : client = BrowserClient()..withCredentials = true; 25 | 26 | Map get _headers => {HttpHeaders.contentTypeHeader: 'application/json'}; 27 | 28 | Uri getUri(String path) => baseUrl.replace(path: '/api$path'); 29 | 30 | void clearAuthCookie() => html.document.cookie = null; 31 | 32 | Future getUser() async { 33 | final result = await _runCatching(() => client.get(getUri('/users/me'), headers: _headers)); 34 | 35 | final data = jsonDecode(result.body)['user']; 36 | return User.fromJson(data); 37 | } 38 | 39 | Future loginUser(String email, String password) async { 40 | final requestBody = jsonEncode({'email': email, 'password': password}); 41 | final result = await _runCatching(() => client.post(getUri('/auth/login'), headers: _headers, body: requestBody)); 42 | 43 | final data = jsonDecode(result.body)['user']; 44 | return User.fromJson(data); 45 | } 46 | 47 | Future registerUser(String displayName, String email, String password) async { 48 | final requestBody = jsonEncode({'name': displayName, 'email': email, 'password': password}); 49 | await _runCatching(() => client.post(getUri('/auth/register'), headers: _headers, body: requestBody)); 50 | 51 | return true; 52 | } 53 | 54 | Future> getArticles() async { 55 | final result = await _runCatching(() => client.get(getUri('/articles'), headers: _headers)); 56 | final items = jsonDecode(result.body)['articles'] as Iterable; 57 | 58 | return items.map((e) { 59 | return (article: Article.fromJson(e), author: User.fromJson(e['author'])); 60 | }).toList(); 61 | } 62 | 63 | Future getArticle(int articleId) async { 64 | final result = await _runCatching(() => client.get(getUri('/articles/$articleId'), headers: _headers)); 65 | final articleData = jsonDecode(result.body)['article']; 66 | 67 | return ( 68 | article: Article.fromJson(articleData), 69 | author: User.fromJson(articleData['author']), 70 | ); 71 | } 72 | 73 | Future
createArticle(String title, String description, String? imageUrl) async { 74 | final dataMap = { 75 | 'title': title, 76 | 'description': description, 77 | if (imageUrl != null && imageUrl.trim().isNotEmpty) 'imageUrl': imageUrl, 78 | }; 79 | 80 | final result = 81 | await _runCatching(() => client.post(getUri('/articles'), headers: _headers, body: jsonEncode(dataMap))); 82 | 83 | final data = jsonDecode(result.body)['article']; 84 | return Article.fromJson(data); 85 | } 86 | 87 | Future
updateArticle(int articleId, String title, String description, String? imageUrl) async { 88 | final requestData = { 89 | 'title': title, 90 | 'description': description, 91 | if (imageUrl != null && imageUrl.trim().isNotEmpty) 'imageUrl': imageUrl, 92 | }; 93 | 94 | final result = await _runCatching( 95 | () => client.put(getUri('/articles/$articleId'), headers: _headers, body: jsonEncode(requestData))); 96 | 97 | final data = jsonDecode(result.body)['article']; 98 | return Article.fromJson(data); 99 | } 100 | 101 | Future deleteArticle(int articleId) async { 102 | await _runCatching(() => client.delete(getUri('/articles/$articleId'), headers: _headers)); 103 | } 104 | 105 | Future _runCatching(HttpResponseCb apiCall) async { 106 | try { 107 | final response = await apiCall.call(); 108 | if (response.statusCode == HttpStatus.ok) return response; 109 | final errors = jsonDecode(response.body)['errors'] as List; 110 | throw ApiException(errors.map((e) => e.toString())); 111 | } on ApiException { 112 | rethrow; 113 | } catch (e) { 114 | throw ApiException([e.toString()]); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/frontend/lib/data/providers/article_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:frontend/data/api_service.dart'; 2 | import 'package:frontend/main.dart'; 3 | import 'package:frontend/utils/provider.dart'; 4 | import 'package:meta/meta.dart'; 5 | import 'package:shared/models.dart'; 6 | 7 | class ArticleProvider extends BaseProvider> { 8 | @visibleForTesting 9 | ApiService get apiSvc => getIt.get(); 10 | 11 | Future fetchArticles() async { 12 | final articles = await safeRun(() => apiSvc.getArticles()); 13 | if (articles == null) return; 14 | 15 | addEvent(ProviderEvent.success(data: articles.map((e) => e.article).toList())); 16 | } 17 | 18 | Future addArticle(String title, String description, String? imageUrl) async { 19 | final articles = lastEvent?.data ?? []; 20 | final article = await safeRun(() => apiSvc.createArticle(title, description, imageUrl)); 21 | if (article == null) return; 22 | 23 | addEvent(ProviderEvent.success(data: [...articles, article])); 24 | } 25 | 26 | Future updateArticle(int articleId, String title, String description, String? imageUrl) async { 27 | final articles = lastEvent?.data ?? []; 28 | final article = await safeRun(() => apiSvc.updateArticle(articleId, title, description, imageUrl)); 29 | if (article == null) return; 30 | 31 | addEvent(ProviderEvent.success(data: [...articles, article])); 32 | } 33 | 34 | Future deleteArticle( 35 | int articleId, 36 | ) async { 37 | final articles = lastEvent?.data ?? []; 38 | await safeRun(() => apiSvc.deleteArticle( 39 | articleId, 40 | )); 41 | 42 | addEvent(ProviderEvent.success(data: [ 43 | ...articles, 44 | ])); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/frontend/lib/data/providers/auth_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:frontend/data/api_service.dart'; 4 | import 'package:frontend/main.dart'; 5 | import 'package:frontend/utils/provider.dart'; 6 | import 'package:localstorage/localstorage.dart'; 7 | import 'package:meta/meta.dart'; 8 | import 'package:shared/models.dart'; 9 | 10 | class AuthProvider extends BaseProvider { 11 | @visibleForTesting 12 | ApiService get apiSvc => getIt.get(); 13 | 14 | final LocalStorage _userLocalStore = LocalStorage('user_session_store'); 15 | static const String userStorageKey = 'user_data'; 16 | 17 | User? get user { 18 | final userFromState = lastEvent?.data; 19 | if (userFromState != null) return userFromState; 20 | final serializedUser = _userLocalStore.getItem(userStorageKey); 21 | return serializedUser == null ? null : User.fromJson(serializedUser); 22 | } 23 | 24 | Future getUser() async { 25 | final user = await safeRun(() => apiSvc.getUser()); 26 | if (user == null) { 27 | logout(); 28 | return; 29 | } 30 | 31 | _setUser(user); 32 | } 33 | 34 | Future login(String email, String password) async { 35 | final user = await safeRun(() => apiSvc.loginUser(email, password)); 36 | if (user == null) return; 37 | 38 | _setUser(user); 39 | } 40 | 41 | Future register(String displayName, String email, String password) async { 42 | final success = await safeRun(() => apiSvc.registerUser(displayName, email, password)); 43 | if (success != true) return false; 44 | 45 | addEvent(const ProviderEvent.idle()); 46 | return true; 47 | } 48 | 49 | void _setUser(User user) { 50 | addEvent(ProviderEvent.success(data: user)); 51 | 52 | _userLocalStore.setItem(userStorageKey, user.toJson()); 53 | } 54 | 55 | void logout() { 56 | apiSvc.clearAuthCookie(); 57 | _userLocalStore.clear(); 58 | addEvent(const ProviderEvent.idle()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/frontend/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:fast_cached_network_image/fast_cached_network_image.dart'; 2 | import 'package:fluent_ui/fluent_ui.dart' hide Colors; 3 | import 'package:flutter/material.dart' show Colors; 4 | import 'package:frontend/blog/widgets/article_form.dart'; 5 | import 'package:frontend/blog/blog_detail.dart'; 6 | import 'package:frontend/blog/blog.dart'; 7 | import 'package:get_it/get_it.dart'; 8 | import 'package:go_router/go_router.dart'; 9 | import 'package:provider/provider.dart'; 10 | import 'package:shared/shared.dart'; 11 | import 'package:url_launcher/url_launcher.dart'; 12 | 13 | import 'auth/auth.dart'; 14 | import 'data/api_service.dart'; 15 | import 'utils/misc.dart'; 16 | 17 | import 'data/providers/auth_provider.dart'; 18 | import 'data/providers/article_provider.dart'; 19 | 20 | final router = GoRouter( 21 | routes: [ 22 | GoRoute(path: '/', builder: (_, __) => const BlogPage()), 23 | GoRoute( 24 | path: '/login', 25 | builder: (_, state) { 26 | final extra = state.extra; 27 | return LoginPage(returnUrl: extra is Map ? extra['returnUrl'] : null); 28 | }, 29 | ), 30 | GoRoute(path: '/register', builder: (_, __) => const RegisterPage(), name: 'register'), 31 | GoRoute(path: '/posts/new', builder: (_, __) => const ArticleFormView()), 32 | GoRoute(path: '/posts/:postId', builder: (_, state) => BlogDetail(state.pathParameters['postId'] ?? '')), 33 | GoRoute( 34 | path: '/posts/:postId/edit', 35 | builder: (_, state) => ArticleFormView(articleId: state.pathParameters['postId'] ?? '')), 36 | ], 37 | ); 38 | 39 | final getIt = GetIt.instance; 40 | 41 | void main() async { 42 | getIt.registerSingleton(ApiService(appEnv.apiURL)); 43 | 44 | await FastCachedImageConfig.init(clearCacheAfter: const Duration(hours: 1)); 45 | 46 | runApp(const MyApp()); 47 | } 48 | 49 | class MyApp extends StatelessWidget { 50 | const MyApp({super.key}); 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return MultiProvider( 55 | providers: [ 56 | ChangeNotifierProvider(create: (_) => AuthProvider()), 57 | ChangeNotifierProvider(create: (_) => ArticleProvider()), 58 | ], 59 | child: FluentApp.router( 60 | routerConfig: router, 61 | title: 'Dart Blog', 62 | debugShowCheckedModeBanner: false, 63 | color: Colors.red, 64 | theme: FluentThemeData.light(), 65 | builder: (_, child) => _AppLayout(child: child ?? const SizedBox.shrink()), 66 | ), 67 | ); 68 | } 69 | } 70 | 71 | class _AppLayout extends StatelessWidget { 72 | final Widget child; 73 | 74 | const _AppLayout({required this.child}); 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return Column( 79 | children: [ 80 | FluentTheme( 81 | data: FluentThemeData.dark(), 82 | child: Container( 83 | decoration: const BoxDecoration(color: Colors.black87), 84 | padding: const EdgeInsets.only(top: 12), 85 | alignment: Alignment.center, 86 | child: PageHeader( 87 | title: GestureDetector( 88 | onTap: () => router.pushReplacement('/'), 89 | child: Row( 90 | children: [ 91 | const Text('Dart Blog', style: TextStyle(fontSize: 20)), 92 | const SizedBox(width: 10), 93 | Image.asset('web/icons/icon-192.png', width: 24, height: 24), 94 | ], 95 | ), 96 | ), 97 | commandBar: Row( 98 | mainAxisAlignment: MainAxisAlignment.end, 99 | children: [ 100 | const AuthHeaderOptions(), 101 | const SizedBox(width: 24), 102 | Tooltip( 103 | message: 'View on Github', 104 | displayHorizontally: true, 105 | useMousePosition: false, 106 | style: const TooltipThemeData(preferBelow: true), 107 | child: IconButton( 108 | icon: const Icon(FluentIcons.graph_symbol, size: 24.0), 109 | onPressed: () => launchUrl(Uri.parse(projectUrl)), 110 | ), 111 | ), 112 | ], 113 | ), 114 | ), 115 | ), 116 | ), 117 | Expanded(child: child), 118 | ], 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/frontend/lib/utils/misc.dart: -------------------------------------------------------------------------------- 1 | import 'package:cherry_toast/cherry_toast.dart'; 2 | import 'package:cherry_toast/resources/arrays.dart'; 3 | import 'package:fast_cached_network_image/fast_cached_network_image.dart'; 4 | import 'package:fluent_ui/fluent_ui.dart'; 5 | 6 | const projectUrl = 'https://github.com/codekeyz/dart-blog'; 7 | 8 | const blogColor = Color(0xff1c2834); 9 | 10 | void showError(BuildContext context, String error) => CherryToast.error( 11 | title: Text( 12 | error, 13 | style: FluentTheme.of(context).typography.bodyLarge!.copyWith(fontSize: 12, color: Colors.white), 14 | ), 15 | toastPosition: Position.bottom, 16 | autoDismiss: true, 17 | borderRadius: 0, 18 | backgroundColor: blogColor, 19 | shadowColor: Colors.transparent, 20 | toastDuration: const Duration(seconds: 5), 21 | displayCloseButton: false, 22 | iconWidget: const Icon(FluentIcons.error, color: Colors.white), 23 | ).show(context); 24 | 25 | const acrylicBackground = Card( 26 | padding: EdgeInsets.zero, 27 | child: SizedBox( 28 | height: double.maxFinite, 29 | width: double.maxFinite, 30 | child: Acrylic(tint: blogColor), 31 | ), 32 | ); 33 | 34 | loadingView({String message = 'loading, please wait...', bool showMessage = true, double? size}) => Container( 35 | alignment: Alignment.center, 36 | child: Column(mainAxisSize: MainAxisSize.min, children: [ 37 | SizedBox(width: size, height: size, child: const ProgressRing(strokeWidth: 2)), 38 | if (showMessage) ...[ 39 | const SizedBox(height: 24), 40 | Text(message, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w300)), 41 | ] 42 | ])); 43 | 44 | errorView({String? message}) { 45 | message ??= 'Oops!, an error occurred'; 46 | return Container( 47 | alignment: Alignment.center, 48 | child: Column( 49 | mainAxisSize: MainAxisSize.min, 50 | children: [ 51 | const Icon(FluentIcons.error), 52 | const SizedBox(height: 24), 53 | Text(message, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w300)), 54 | ], 55 | ), 56 | ); 57 | } 58 | 59 | imageView(String imageUrl, {double? width, double? height}) => FastCachedImage( 60 | url: imageUrl, 61 | fit: BoxFit.cover, 62 | errorBuilder: (_, error, df) => const Center( 63 | child: Text('An error occurred while loading image'), 64 | ), 65 | height: height ?? double.maxFinite, 66 | width: width ?? double.maxFinite, 67 | fadeInDuration: const Duration(seconds: 1), 68 | loadingBuilder: (p0, p1) => loadingView(showMessage: false, size: 24), 69 | ); 70 | -------------------------------------------------------------------------------- /packages/frontend/lib/utils/provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | 4 | import '../data/api_service.dart'; 5 | 6 | enum ProviderState { idle, loading, success, error } 7 | 8 | class ProviderEvent { 9 | final T? data; 10 | final ProviderState state; 11 | final String? errorMessage; 12 | 13 | const ProviderEvent.idle() 14 | : state = ProviderState.idle, 15 | data = null, 16 | errorMessage = null; 17 | 18 | const ProviderEvent.loading({this.data}) 19 | : state = ProviderState.loading, 20 | errorMessage = null; 21 | 22 | const ProviderEvent.success({required this.data}) 23 | : state = ProviderState.success, 24 | errorMessage = null; 25 | 26 | const ProviderEvent.error({required this.errorMessage}) 27 | : state = ProviderState.error, 28 | data = null; 29 | } 30 | 31 | mixin DataStreamMixin { 32 | final _streamController = StreamController.broadcast(); 33 | 34 | /// access the stream 35 | Stream get stream => _streamController.stream; 36 | 37 | /// access the sink 38 | @protected 39 | Sink get sink => _streamController.sink; 40 | 41 | /// 42 | T? _lastEvent; 43 | 44 | /// access the last event sent into the stream 45 | T? get lastEvent => _lastEvent; 46 | 47 | /// adds an event into the stream 48 | /// also stores is as a [lastEvent] 49 | /// and notifies state 50 | @protected 51 | void addEvent(T event) { 52 | _lastEvent = event; 53 | sink.add(event); 54 | } 55 | 56 | /// clear lastevent 57 | void clear() { 58 | _lastEvent = null; 59 | } 60 | 61 | /// close the stream 62 | void dispose() { 63 | _streamController.close(); 64 | } 65 | } 66 | 67 | abstract class BaseProvider extends ChangeNotifier with DataStreamMixin> { 68 | ProviderState? get state => lastEvent?.state; 69 | 70 | bool get isLoading => state == ProviderState.loading; 71 | 72 | bool get hasError => state == ProviderState.error; 73 | 74 | String? get errorMessage => lastEvent?.errorMessage; 75 | 76 | @override 77 | void clear() { 78 | super.clear(); 79 | addEvent(const ProviderEvent.idle()); 80 | } 81 | 82 | Future safeRun(FutureOr Function() func) async { 83 | addEvent(const ProviderEvent.loading()); 84 | 85 | try { 86 | return await func.call(); 87 | } on ApiException catch (e) { 88 | addEvent(ProviderEvent.error(errorMessage: e.errors.join('\n'))); 89 | return null; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/frontend/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "72.0.0" 12 | _macros: 13 | dependency: transitive 14 | description: dart 15 | source: sdk 16 | version: "0.3.2" 17 | analyzer: 18 | dependency: transitive 19 | description: 20 | name: analyzer 21 | sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 22 | url: "https://pub.dev" 23 | source: hosted 24 | version: "6.7.0" 25 | appflowy_editor: 26 | dependency: "direct main" 27 | description: 28 | name: appflowy_editor 29 | sha256: "568a9e73315442157fdd0ff892aa5abed418bec0c1bf8885bb352c26d078a57f" 30 | url: "https://pub.dev" 31 | source: hosted 32 | version: "4.0.0" 33 | archive: 34 | dependency: transitive 35 | description: 36 | name: archive 37 | sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d 38 | url: "https://pub.dev" 39 | source: hosted 40 | version: "3.6.1" 41 | args: 42 | dependency: transitive 43 | description: 44 | name: args 45 | sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 46 | url: "https://pub.dev" 47 | source: hosted 48 | version: "2.6.0" 49 | async: 50 | dependency: transitive 51 | description: 52 | name: async 53 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 54 | url: "https://pub.dev" 55 | source: hosted 56 | version: "2.12.0" 57 | barcode: 58 | dependency: transitive 59 | description: 60 | name: barcode 61 | sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 62 | url: "https://pub.dev" 63 | source: hosted 64 | version: "2.2.8" 65 | bidi: 66 | dependency: transitive 67 | description: 68 | name: bidi 69 | sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" 70 | url: "https://pub.dev" 71 | source: hosted 72 | version: "2.0.12" 73 | boolean_selector: 74 | dependency: transitive 75 | description: 76 | name: boolean_selector 77 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 78 | url: "https://pub.dev" 79 | source: hosted 80 | version: "2.1.2" 81 | build: 82 | dependency: transitive 83 | description: 84 | name: build 85 | sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" 86 | url: "https://pub.dev" 87 | source: hosted 88 | version: "2.4.1" 89 | build_config: 90 | dependency: transitive 91 | description: 92 | name: build_config 93 | sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 94 | url: "https://pub.dev" 95 | source: hosted 96 | version: "1.1.1" 97 | build_daemon: 98 | dependency: transitive 99 | description: 100 | name: build_daemon 101 | sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" 102 | url: "https://pub.dev" 103 | source: hosted 104 | version: "4.0.2" 105 | build_resolvers: 106 | dependency: transitive 107 | description: 108 | name: build_resolvers 109 | sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" 110 | url: "https://pub.dev" 111 | source: hosted 112 | version: "2.4.2" 113 | build_runner: 114 | dependency: "direct dev" 115 | description: 116 | name: build_runner 117 | sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" 118 | url: "https://pub.dev" 119 | source: hosted 120 | version: "2.4.13" 121 | build_runner_core: 122 | dependency: transitive 123 | description: 124 | name: build_runner_core 125 | sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 126 | url: "https://pub.dev" 127 | source: hosted 128 | version: "7.3.2" 129 | built_collection: 130 | dependency: transitive 131 | description: 132 | name: built_collection 133 | sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 134 | url: "https://pub.dev" 135 | source: hosted 136 | version: "5.1.1" 137 | built_value: 138 | dependency: transitive 139 | description: 140 | name: built_value 141 | sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb 142 | url: "https://pub.dev" 143 | source: hosted 144 | version: "8.9.2" 145 | characters: 146 | dependency: transitive 147 | description: 148 | name: characters 149 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 150 | url: "https://pub.dev" 151 | source: hosted 152 | version: "1.3.0" 153 | charcode: 154 | dependency: transitive 155 | description: 156 | name: charcode 157 | sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 158 | url: "https://pub.dev" 159 | source: hosted 160 | version: "1.3.1" 161 | checked_yaml: 162 | dependency: transitive 163 | description: 164 | name: checked_yaml 165 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 166 | url: "https://pub.dev" 167 | source: hosted 168 | version: "2.0.3" 169 | cherry_toast: 170 | dependency: "direct main" 171 | description: 172 | name: cherry_toast 173 | sha256: b2d67085fa1d533f41ef6d079a83b01084a5e508d20c958636a3487f14e6f839 174 | url: "https://pub.dev" 175 | source: hosted 176 | version: "1.11.0" 177 | clock: 178 | dependency: transitive 179 | description: 180 | name: clock 181 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 182 | url: "https://pub.dev" 183 | source: hosted 184 | version: "1.1.1" 185 | code_builder: 186 | dependency: transitive 187 | description: 188 | name: code_builder 189 | sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" 190 | url: "https://pub.dev" 191 | source: hosted 192 | version: "4.10.1" 193 | collection: 194 | dependency: "direct main" 195 | description: 196 | name: collection 197 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 198 | url: "https://pub.dev" 199 | source: hosted 200 | version: "1.18.0" 201 | convert: 202 | dependency: transitive 203 | description: 204 | name: convert 205 | sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 206 | url: "https://pub.dev" 207 | source: hosted 208 | version: "3.1.2" 209 | cross_file: 210 | dependency: transitive 211 | description: 212 | name: cross_file 213 | sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" 214 | url: "https://pub.dev" 215 | source: hosted 216 | version: "0.3.4+2" 217 | crypto: 218 | dependency: transitive 219 | description: 220 | name: crypto 221 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 222 | url: "https://pub.dev" 223 | source: hosted 224 | version: "3.0.6" 225 | csslib: 226 | dependency: transitive 227 | description: 228 | name: csslib 229 | sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" 230 | url: "https://pub.dev" 231 | source: hosted 232 | version: "1.0.2" 233 | dart_style: 234 | dependency: transitive 235 | description: 236 | name: dart_style 237 | sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" 238 | url: "https://pub.dev" 239 | source: hosted 240 | version: "2.3.7" 241 | device_info_plus: 242 | dependency: transitive 243 | description: 244 | name: device_info_plus 245 | sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 246 | url: "https://pub.dev" 247 | source: hosted 248 | version: "10.1.2" 249 | device_info_plus_platform_interface: 250 | dependency: transitive 251 | description: 252 | name: device_info_plus_platform_interface 253 | sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" 254 | url: "https://pub.dev" 255 | source: hosted 256 | version: "7.0.1" 257 | diff_match_patch: 258 | dependency: transitive 259 | description: 260 | name: diff_match_patch 261 | sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" 262 | url: "https://pub.dev" 263 | source: hosted 264 | version: "0.4.1" 265 | dio: 266 | dependency: transitive 267 | description: 268 | name: dio 269 | sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" 270 | url: "https://pub.dev" 271 | source: hosted 272 | version: "5.7.0" 273 | dio_web_adapter: 274 | dependency: transitive 275 | description: 276 | name: dio_web_adapter 277 | sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" 278 | url: "https://pub.dev" 279 | source: hosted 280 | version: "2.0.0" 281 | fast_cached_network_image: 282 | dependency: "direct main" 283 | description: 284 | name: fast_cached_network_image 285 | sha256: "91f1d48d10e2916b83a1e7545c1eaf752f85b32acfb1473be1f9fa51d73afef0" 286 | url: "https://pub.dev" 287 | source: hosted 288 | version: "1.2.9" 289 | ffi: 290 | dependency: transitive 291 | description: 292 | name: ffi 293 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 294 | url: "https://pub.dev" 295 | source: hosted 296 | version: "2.1.3" 297 | file: 298 | dependency: transitive 299 | description: 300 | name: file 301 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 302 | url: "https://pub.dev" 303 | source: hosted 304 | version: "7.0.1" 305 | file_picker: 306 | dependency: transitive 307 | description: 308 | name: file_picker 309 | sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877 310 | url: "https://pub.dev" 311 | source: hosted 312 | version: "8.1.3" 313 | fixnum: 314 | dependency: transitive 315 | description: 316 | name: fixnum 317 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 318 | url: "https://pub.dev" 319 | source: hosted 320 | version: "1.1.1" 321 | fluent_ui: 322 | dependency: "direct main" 323 | description: 324 | name: fluent_ui 325 | sha256: e7804bf3bbb3ecf9e77d5498181dc36375f5ca736ccfb3862fea17c50050eb89 326 | url: "https://pub.dev" 327 | source: hosted 328 | version: "4.9.2" 329 | flutter: 330 | dependency: "direct main" 331 | description: flutter 332 | source: sdk 333 | version: "0.0.0" 334 | flutter_highlight: 335 | dependency: transitive 336 | description: 337 | name: flutter_highlight 338 | sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" 339 | url: "https://pub.dev" 340 | source: hosted 341 | version: "0.7.0" 342 | flutter_lints: 343 | dependency: "direct dev" 344 | description: 345 | name: flutter_lints 346 | sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" 347 | url: "https://pub.dev" 348 | source: hosted 349 | version: "3.0.2" 350 | flutter_localizations: 351 | dependency: transitive 352 | description: flutter 353 | source: sdk 354 | version: "0.0.0" 355 | flutter_plugin_android_lifecycle: 356 | dependency: transitive 357 | description: 358 | name: flutter_plugin_android_lifecycle 359 | sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" 360 | url: "https://pub.dev" 361 | source: hosted 362 | version: "2.0.23" 363 | flutter_svg: 364 | dependency: transitive 365 | description: 366 | name: flutter_svg 367 | sha256: "1b7723a814d84fb65869ea7115cdb3ee7c3be5a27a755c1ec60e049f6b9fcbb2" 368 | url: "https://pub.dev" 369 | source: hosted 370 | version: "2.0.11" 371 | flutter_web_plugins: 372 | dependency: transitive 373 | description: flutter 374 | source: sdk 375 | version: "0.0.0" 376 | frontend_server_client: 377 | dependency: transitive 378 | description: 379 | name: frontend_server_client 380 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 381 | url: "https://pub.dev" 382 | source: hosted 383 | version: "4.0.0" 384 | get_it: 385 | dependency: "direct main" 386 | description: 387 | name: get_it 388 | sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 389 | url: "https://pub.dev" 390 | source: hosted 391 | version: "7.7.0" 392 | glob: 393 | dependency: transitive 394 | description: 395 | name: glob 396 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 397 | url: "https://pub.dev" 398 | source: hosted 399 | version: "2.1.2" 400 | go_router: 401 | dependency: "direct main" 402 | description: 403 | name: go_router 404 | sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 405 | url: "https://pub.dev" 406 | source: hosted 407 | version: "13.2.5" 408 | graphs: 409 | dependency: transitive 410 | description: 411 | name: graphs 412 | sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" 413 | url: "https://pub.dev" 414 | source: hosted 415 | version: "2.3.2" 416 | highlight: 417 | dependency: transitive 418 | description: 419 | name: highlight 420 | sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" 421 | url: "https://pub.dev" 422 | source: hosted 423 | version: "0.7.0" 424 | hive: 425 | dependency: transitive 426 | description: 427 | name: hive 428 | sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" 429 | url: "https://pub.dev" 430 | source: hosted 431 | version: "2.2.3" 432 | hive_flutter: 433 | dependency: transitive 434 | description: 435 | name: hive_flutter 436 | sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "1.1.0" 440 | html: 441 | dependency: transitive 442 | description: 443 | name: html 444 | sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "0.15.5" 448 | http: 449 | dependency: "direct main" 450 | description: 451 | name: http 452 | sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "1.2.2" 456 | http_multi_server: 457 | dependency: transitive 458 | description: 459 | name: http_multi_server 460 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "3.2.1" 464 | http_parser: 465 | dependency: transitive 466 | description: 467 | name: http_parser 468 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "4.0.2" 472 | image: 473 | dependency: transitive 474 | description: 475 | name: image 476 | sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "4.3.0" 480 | intl: 481 | dependency: transitive 482 | description: 483 | name: intl 484 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "0.19.0" 488 | io: 489 | dependency: transitive 490 | description: 491 | name: io 492 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "1.0.4" 496 | js: 497 | dependency: transitive 498 | description: 499 | name: js 500 | sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "0.7.1" 504 | json_annotation: 505 | dependency: "direct main" 506 | description: 507 | name: json_annotation 508 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "4.9.0" 512 | json_serializable: 513 | dependency: "direct dev" 514 | description: 515 | name: json_serializable 516 | sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "6.8.0" 520 | keyboard_height_plugin: 521 | dependency: transitive 522 | description: 523 | name: keyboard_height_plugin 524 | sha256: "3a51c8ebb43465ebe0b3bad17f3b6d945421e58011f3f5a08134afe69a3d775f" 525 | url: "https://pub.dev" 526 | source: hosted 527 | version: "0.1.5" 528 | lints: 529 | dependency: transitive 530 | description: 531 | name: lints 532 | sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 533 | url: "https://pub.dev" 534 | source: hosted 535 | version: "3.0.0" 536 | localstorage: 537 | dependency: "direct main" 538 | description: 539 | name: localstorage 540 | sha256: fdff4f717114e992acfd4045dc4a9ab9b987ca57f020965d63e3eb34089c60d8 541 | url: "https://pub.dev" 542 | source: hosted 543 | version: "4.0.1+4" 544 | logging: 545 | dependency: transitive 546 | description: 547 | name: logging 548 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 549 | url: "https://pub.dev" 550 | source: hosted 551 | version: "1.3.0" 552 | macros: 553 | dependency: transitive 554 | description: 555 | name: macros 556 | sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" 557 | url: "https://pub.dev" 558 | source: hosted 559 | version: "0.1.2-main.4" 560 | markdown: 561 | dependency: transitive 562 | description: 563 | name: markdown 564 | sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 565 | url: "https://pub.dev" 566 | source: hosted 567 | version: "7.2.2" 568 | markdown_widget: 569 | dependency: "direct main" 570 | description: 571 | name: markdown_widget 572 | sha256: "216dced98962d7699a265344624bc280489d739654585ee881c95563a3252fac" 573 | url: "https://pub.dev" 574 | source: hosted 575 | version: "2.3.2+6" 576 | matcher: 577 | dependency: transitive 578 | description: 579 | name: matcher 580 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 581 | url: "https://pub.dev" 582 | source: hosted 583 | version: "0.12.16+1" 584 | material_color_utilities: 585 | dependency: transitive 586 | description: 587 | name: material_color_utilities 588 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 589 | url: "https://pub.dev" 590 | source: hosted 591 | version: "0.11.1" 592 | math_expressions: 593 | dependency: transitive 594 | description: 595 | name: math_expressions 596 | sha256: e32d803d758ace61cc6c4bdfed1226ff60a6a23646b35685670d28b5616139f8 597 | url: "https://pub.dev" 598 | source: hosted 599 | version: "2.6.0" 600 | meta: 601 | dependency: "direct main" 602 | description: 603 | name: meta 604 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 605 | url: "https://pub.dev" 606 | source: hosted 607 | version: "1.15.0" 608 | mime: 609 | dependency: transitive 610 | description: 611 | name: mime 612 | sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" 613 | url: "https://pub.dev" 614 | source: hosted 615 | version: "2.0.0" 616 | nanoid: 617 | dependency: transitive 618 | description: 619 | name: nanoid 620 | sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e 621 | url: "https://pub.dev" 622 | source: hosted 623 | version: "1.0.0" 624 | nested: 625 | dependency: transitive 626 | description: 627 | name: nested 628 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 629 | url: "https://pub.dev" 630 | source: hosted 631 | version: "1.0.0" 632 | numerus: 633 | dependency: transitive 634 | description: 635 | name: numerus 636 | sha256: a17a3f34527497e89378696a76f382b40dc534c4a57b3778de246ebc1ce2ca99 637 | url: "https://pub.dev" 638 | source: hosted 639 | version: "2.3.0" 640 | package_config: 641 | dependency: transitive 642 | description: 643 | name: package_config 644 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 645 | url: "https://pub.dev" 646 | source: hosted 647 | version: "2.1.0" 648 | path: 649 | dependency: transitive 650 | description: 651 | name: path 652 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 653 | url: "https://pub.dev" 654 | source: hosted 655 | version: "1.9.0" 656 | path_parsing: 657 | dependency: transitive 658 | description: 659 | name: path_parsing 660 | sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" 661 | url: "https://pub.dev" 662 | source: hosted 663 | version: "1.1.0" 664 | path_provider: 665 | dependency: transitive 666 | description: 667 | name: path_provider 668 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 669 | url: "https://pub.dev" 670 | source: hosted 671 | version: "2.1.5" 672 | path_provider_android: 673 | dependency: transitive 674 | description: 675 | name: path_provider_android 676 | sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a 677 | url: "https://pub.dev" 678 | source: hosted 679 | version: "2.2.12" 680 | path_provider_foundation: 681 | dependency: transitive 682 | description: 683 | name: path_provider_foundation 684 | sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 685 | url: "https://pub.dev" 686 | source: hosted 687 | version: "2.4.0" 688 | path_provider_linux: 689 | dependency: transitive 690 | description: 691 | name: path_provider_linux 692 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 693 | url: "https://pub.dev" 694 | source: hosted 695 | version: "2.2.1" 696 | path_provider_platform_interface: 697 | dependency: transitive 698 | description: 699 | name: path_provider_platform_interface 700 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 701 | url: "https://pub.dev" 702 | source: hosted 703 | version: "2.1.2" 704 | path_provider_windows: 705 | dependency: transitive 706 | description: 707 | name: path_provider_windows 708 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 709 | url: "https://pub.dev" 710 | source: hosted 711 | version: "2.3.0" 712 | pdf: 713 | dependency: transitive 714 | description: 715 | name: pdf 716 | sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" 717 | url: "https://pub.dev" 718 | source: hosted 719 | version: "3.11.1" 720 | petitparser: 721 | dependency: transitive 722 | description: 723 | name: petitparser 724 | sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 725 | url: "https://pub.dev" 726 | source: hosted 727 | version: "6.0.2" 728 | platform: 729 | dependency: transitive 730 | description: 731 | name: platform 732 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 733 | url: "https://pub.dev" 734 | source: hosted 735 | version: "3.1.6" 736 | plugin_platform_interface: 737 | dependency: transitive 738 | description: 739 | name: plugin_platform_interface 740 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 741 | url: "https://pub.dev" 742 | source: hosted 743 | version: "2.1.8" 744 | pool: 745 | dependency: transitive 746 | description: 747 | name: pool 748 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 749 | url: "https://pub.dev" 750 | source: hosted 751 | version: "1.5.1" 752 | provider: 753 | dependency: "direct main" 754 | description: 755 | name: provider 756 | sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c 757 | url: "https://pub.dev" 758 | source: hosted 759 | version: "6.1.2" 760 | pub_semver: 761 | dependency: transitive 762 | description: 763 | name: pub_semver 764 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 765 | url: "https://pub.dev" 766 | source: hosted 767 | version: "2.1.4" 768 | pubspec_parse: 769 | dependency: transitive 770 | description: 771 | name: pubspec_parse 772 | sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 773 | url: "https://pub.dev" 774 | source: hosted 775 | version: "1.3.0" 776 | qr: 777 | dependency: transitive 778 | description: 779 | name: qr 780 | sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" 781 | url: "https://pub.dev" 782 | source: hosted 783 | version: "3.0.2" 784 | recase: 785 | dependency: transitive 786 | description: 787 | name: recase 788 | sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 789 | url: "https://pub.dev" 790 | source: hosted 791 | version: "4.1.0" 792 | scroll_pos: 793 | dependency: transitive 794 | description: 795 | name: scroll_pos 796 | sha256: cebf602b2dd939de6832bb902ffefb574608d1b84f420b82b381a4007d3c1e1b 797 | url: "https://pub.dev" 798 | source: hosted 799 | version: "0.5.0" 800 | scroll_to_index: 801 | dependency: transitive 802 | description: 803 | name: scroll_to_index 804 | sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 805 | url: "https://pub.dev" 806 | source: hosted 807 | version: "3.0.1" 808 | shared: 809 | dependency: "direct main" 810 | description: 811 | path: "../shared" 812 | relative: true 813 | source: path 814 | version: "1.0.0" 815 | shelf: 816 | dependency: transitive 817 | description: 818 | name: shelf 819 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 820 | url: "https://pub.dev" 821 | source: hosted 822 | version: "1.4.1" 823 | shelf_web_socket: 824 | dependency: transitive 825 | description: 826 | name: shelf_web_socket 827 | sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" 828 | url: "https://pub.dev" 829 | source: hosted 830 | version: "2.0.0" 831 | sky_engine: 832 | dependency: transitive 833 | description: flutter 834 | source: sdk 835 | version: "0.0.99" 836 | source_gen: 837 | dependency: transitive 838 | description: 839 | name: source_gen 840 | sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" 841 | url: "https://pub.dev" 842 | source: hosted 843 | version: "1.5.0" 844 | source_helper: 845 | dependency: transitive 846 | description: 847 | name: source_helper 848 | sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" 849 | url: "https://pub.dev" 850 | source: hosted 851 | version: "1.3.4" 852 | source_span: 853 | dependency: transitive 854 | description: 855 | name: source_span 856 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 857 | url: "https://pub.dev" 858 | source: hosted 859 | version: "1.10.0" 860 | sprintf: 861 | dependency: transitive 862 | description: 863 | name: sprintf 864 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 865 | url: "https://pub.dev" 866 | source: hosted 867 | version: "7.0.0" 868 | stack_trace: 869 | dependency: transitive 870 | description: 871 | name: stack_trace 872 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 873 | url: "https://pub.dev" 874 | source: hosted 875 | version: "1.12.0" 876 | stream_channel: 877 | dependency: transitive 878 | description: 879 | name: stream_channel 880 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 881 | url: "https://pub.dev" 882 | source: hosted 883 | version: "2.1.2" 884 | stream_transform: 885 | dependency: transitive 886 | description: 887 | name: stream_transform 888 | sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" 889 | url: "https://pub.dev" 890 | source: hosted 891 | version: "2.1.0" 892 | string_scanner: 893 | dependency: transitive 894 | description: 895 | name: string_scanner 896 | sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" 897 | url: "https://pub.dev" 898 | source: hosted 899 | version: "1.4.0" 900 | string_validator: 901 | dependency: transitive 902 | description: 903 | name: string_validator 904 | sha256: a278d038104aa2df15d0e09c47cb39a49f907260732067d0034dc2f2e4e2ac94 905 | url: "https://pub.dev" 906 | source: hosted 907 | version: "1.1.0" 908 | term_glyph: 909 | dependency: transitive 910 | description: 911 | name: term_glyph 912 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 913 | url: "https://pub.dev" 914 | source: hosted 915 | version: "1.2.1" 916 | test_api: 917 | dependency: transitive 918 | description: 919 | name: test_api 920 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 921 | url: "https://pub.dev" 922 | source: hosted 923 | version: "0.7.3" 924 | timeago: 925 | dependency: "direct main" 926 | description: 927 | name: timeago 928 | sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de" 929 | url: "https://pub.dev" 930 | source: hosted 931 | version: "3.7.0" 932 | timing: 933 | dependency: transitive 934 | description: 935 | name: timing 936 | sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" 937 | url: "https://pub.dev" 938 | source: hosted 939 | version: "1.0.1" 940 | typed_data: 941 | dependency: transitive 942 | description: 943 | name: typed_data 944 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 945 | url: "https://pub.dev" 946 | source: hosted 947 | version: "1.4.0" 948 | universal_html: 949 | dependency: transitive 950 | description: 951 | name: universal_html 952 | sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" 953 | url: "https://pub.dev" 954 | source: hosted 955 | version: "2.2.4" 956 | universal_io: 957 | dependency: transitive 958 | description: 959 | name: universal_io 960 | sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" 961 | url: "https://pub.dev" 962 | source: hosted 963 | version: "2.2.2" 964 | universal_platform: 965 | dependency: transitive 966 | description: 967 | name: universal_platform 968 | sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" 969 | url: "https://pub.dev" 970 | source: hosted 971 | version: "1.1.0" 972 | url_launcher: 973 | dependency: "direct main" 974 | description: 975 | name: url_launcher 976 | sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" 977 | url: "https://pub.dev" 978 | source: hosted 979 | version: "6.3.1" 980 | url_launcher_android: 981 | dependency: transitive 982 | description: 983 | name: url_launcher_android 984 | sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" 985 | url: "https://pub.dev" 986 | source: hosted 987 | version: "6.3.14" 988 | url_launcher_ios: 989 | dependency: transitive 990 | description: 991 | name: url_launcher_ios 992 | sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e 993 | url: "https://pub.dev" 994 | source: hosted 995 | version: "6.3.1" 996 | url_launcher_linux: 997 | dependency: transitive 998 | description: 999 | name: url_launcher_linux 1000 | sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af 1001 | url: "https://pub.dev" 1002 | source: hosted 1003 | version: "3.2.0" 1004 | url_launcher_macos: 1005 | dependency: transitive 1006 | description: 1007 | name: url_launcher_macos 1008 | sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" 1009 | url: "https://pub.dev" 1010 | source: hosted 1011 | version: "3.2.1" 1012 | url_launcher_platform_interface: 1013 | dependency: transitive 1014 | description: 1015 | name: url_launcher_platform_interface 1016 | sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" 1017 | url: "https://pub.dev" 1018 | source: hosted 1019 | version: "2.3.2" 1020 | url_launcher_web: 1021 | dependency: transitive 1022 | description: 1023 | name: url_launcher_web 1024 | sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" 1025 | url: "https://pub.dev" 1026 | source: hosted 1027 | version: "2.3.3" 1028 | url_launcher_windows: 1029 | dependency: transitive 1030 | description: 1031 | name: url_launcher_windows 1032 | sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" 1033 | url: "https://pub.dev" 1034 | source: hosted 1035 | version: "3.1.3" 1036 | uuid: 1037 | dependency: transitive 1038 | description: 1039 | name: uuid 1040 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 1041 | url: "https://pub.dev" 1042 | source: hosted 1043 | version: "4.5.1" 1044 | vector_graphics: 1045 | dependency: transitive 1046 | description: 1047 | name: vector_graphics 1048 | sha256: "0b9149c6ddb013818075b072b9ddc1b89a5122fff1275d4648d297086b46c4f0" 1049 | url: "https://pub.dev" 1050 | source: hosted 1051 | version: "1.1.12" 1052 | vector_graphics_codec: 1053 | dependency: transitive 1054 | description: 1055 | name: vector_graphics_codec 1056 | sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" 1057 | url: "https://pub.dev" 1058 | source: hosted 1059 | version: "1.1.12" 1060 | vector_graphics_compiler: 1061 | dependency: transitive 1062 | description: 1063 | name: vector_graphics_compiler 1064 | sha256: f3b9b6e4591c11394d4be4806c63e72d3a41778547b2c1e2a8a04fadcfd7d173 1065 | url: "https://pub.dev" 1066 | source: hosted 1067 | version: "1.1.12" 1068 | vector_math: 1069 | dependency: transitive 1070 | description: 1071 | name: vector_math 1072 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 1073 | url: "https://pub.dev" 1074 | source: hosted 1075 | version: "2.1.4" 1076 | visibility_detector: 1077 | dependency: transitive 1078 | description: 1079 | name: visibility_detector 1080 | sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 1081 | url: "https://pub.dev" 1082 | source: hosted 1083 | version: "0.4.0+2" 1084 | watcher: 1085 | dependency: transitive 1086 | description: 1087 | name: watcher 1088 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 1089 | url: "https://pub.dev" 1090 | source: hosted 1091 | version: "1.1.0" 1092 | web: 1093 | dependency: transitive 1094 | description: 1095 | name: web 1096 | sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb 1097 | url: "https://pub.dev" 1098 | source: hosted 1099 | version: "1.1.0" 1100 | web_socket: 1101 | dependency: transitive 1102 | description: 1103 | name: web_socket 1104 | sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" 1105 | url: "https://pub.dev" 1106 | source: hosted 1107 | version: "0.1.6" 1108 | web_socket_channel: 1109 | dependency: transitive 1110 | description: 1111 | name: web_socket_channel 1112 | sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" 1113 | url: "https://pub.dev" 1114 | source: hosted 1115 | version: "3.0.1" 1116 | win32: 1117 | dependency: transitive 1118 | description: 1119 | name: win32 1120 | sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" 1121 | url: "https://pub.dev" 1122 | source: hosted 1123 | version: "5.8.0" 1124 | win32_registry: 1125 | dependency: transitive 1126 | description: 1127 | name: win32_registry 1128 | sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" 1129 | url: "https://pub.dev" 1130 | source: hosted 1131 | version: "1.1.5" 1132 | xdg_directories: 1133 | dependency: transitive 1134 | description: 1135 | name: xdg_directories 1136 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 1137 | url: "https://pub.dev" 1138 | source: hosted 1139 | version: "1.1.0" 1140 | xml: 1141 | dependency: transitive 1142 | description: 1143 | name: xml 1144 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 1145 | url: "https://pub.dev" 1146 | source: hosted 1147 | version: "6.5.0" 1148 | yaml: 1149 | dependency: transitive 1150 | description: 1151 | name: yaml 1152 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 1153 | url: "https://pub.dev" 1154 | source: hosted 1155 | version: "3.1.2" 1156 | sdks: 1157 | dart: ">=3.5.0 <4.0.0" 1158 | flutter: ">=3.24.0" 1159 | -------------------------------------------------------------------------------- /packages/frontend/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: frontend 2 | description: "A new Flutter project." 3 | publish_to: "none" 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ^3.2.0 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | go_router: ^13.0.1 14 | fluent_ui: ^4.8.3 15 | url_launcher: 16 | cherry_toast: ^1.6.4 17 | 18 | http: 19 | json_annotation: ^4.8.1 20 | provider: ^6.1.1 21 | meta: 22 | get_it: ^7.6.4 23 | collection: 24 | timeago: ^3.6.0 25 | fast_cached_network_image: ^1.2.0 26 | markdown_widget: 27 | appflowy_editor: 28 | localstorage: ^4.0.1+4 29 | shared: 30 | path: '../shared' 31 | 32 | dev_dependencies: 33 | flutter_lints: ^3.0.1 34 | json_serializable: ^6.7.1 35 | build_runner: ^2.4.7 36 | 37 | flutter: 38 | uses-material-design: true 39 | 40 | assets: 41 | - web/icons/ 42 | -------------------------------------------------------------------------------- /packages/frontend/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/packages/frontend/web/favicon.ico -------------------------------------------------------------------------------- /packages/frontend/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/packages/frontend/web/favicon.png -------------------------------------------------------------------------------- /packages/frontend/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/packages/frontend/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /packages/frontend/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/packages/frontend/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /packages/frontend/web/icons/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/packages/frontend/web/icons/icon-192.png -------------------------------------------------------------------------------- /packages/frontend/web/icons/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/packages/frontend/web/icons/icon-512.png -------------------------------------------------------------------------------- /packages/frontend/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Dart Blog 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /packages/frontend/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dart Blog", 3 | "short_name": "dart-blog", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#1c2834", 7 | "theme_color": "#1c2834", 8 | "description": "Full-stack blog show-casing Dart on the Backend and Flutter Web on the web.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/shared/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/packages/shared/.DS_Store -------------------------------------------------------------------------------- /packages/shared/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /packages/shared/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /packages/shared/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /packages/shared/lib/models.dart: -------------------------------------------------------------------------------- 1 | export 'src/models/article.dart'; 2 | export 'src/models/user.dart'; 3 | -------------------------------------------------------------------------------- /packages/shared/lib/shared.dart: -------------------------------------------------------------------------------- 1 | enum AppEnvironment { 2 | local, 3 | staging, 4 | prod; 5 | 6 | const AppEnvironment(); 7 | 8 | Uri get apiURL => switch (this) { 9 | AppEnvironment.prod => Uri.https('blog-backend-369d.globeapp.dev'), 10 | _ => Uri.http('localhost:3000'), 11 | }; 12 | 13 | Uri get frontendURL => switch (this) { 14 | AppEnvironment.prod => Uri.https('blog-frontend.globeapp.dev'), 15 | _ => Uri.http('localhost:60964'), 16 | }; 17 | } 18 | 19 | final isDebugMode = const bool.fromEnvironment("dart.vm.product") == false; 20 | 21 | const defaultArticleImage = 'https://images.pexels.com/photos/261763/pexels-photo-261763.jpeg'; 22 | 23 | bool get isTestMode { 24 | var isDebug = false; 25 | assert(() { 26 | isDebug = true; 27 | return true; 28 | }()); 29 | return isDebug; 30 | } 31 | 32 | final appEnv = isTestMode || isDebugMode ? AppEnvironment.local : AppEnvironment.prod; 33 | -------------------------------------------------------------------------------- /packages/shared/lib/src/models/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'article.g.dart'; 4 | 5 | @JsonSerializable(fieldRename: FieldRename.snake) 6 | class Article { 7 | final int id; 8 | 9 | final String title; 10 | final String description; 11 | 12 | final String? imageUrl; 13 | 14 | final int ownerId; 15 | 16 | final DateTime createdAt; 17 | final DateTime updatedAt; 18 | 19 | const Article( 20 | this.id, 21 | this.title, 22 | this.ownerId, 23 | this.description, { 24 | this.imageUrl, 25 | required this.createdAt, 26 | required this.updatedAt, 27 | }); 28 | 29 | Map toJson() => _$ArticleToJson(this); 30 | 31 | factory Article.fromJson(Map json) => _$ArticleFromJson(json); 32 | } 33 | -------------------------------------------------------------------------------- /packages/shared/lib/src/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'user.g.dart'; 4 | 5 | @JsonSerializable(fieldRename: FieldRename.snake) 6 | class User { 7 | final int id; 8 | 9 | final String name; 10 | 11 | final String email; 12 | 13 | final DateTime createdAt; 14 | 15 | final DateTime updatedAt; 16 | 17 | const User( 18 | this.id, 19 | this.name, 20 | this.email, { 21 | required this.createdAt, 22 | required this.updatedAt, 23 | }); 24 | 25 | Map toJson() => _$UserToJson(this); 26 | 27 | factory User.fromJson(Map json) => _$UserFromJson(json); 28 | } 29 | -------------------------------------------------------------------------------- /packages/shared/melos_shared.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/shared/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: shared 2 | description: A starting point for Dart libraries or applications. 3 | version: 1.0.0 4 | # repository: https://github.com/my_org/my_repo 5 | 6 | environment: 7 | sdk: ^3.2.0 8 | 9 | dependencies: 10 | json_annotation: ^4.9.0 11 | 12 | dev_dependencies: 13 | lints: ^4.0.0 14 | test: ^1.24.0 15 | json_serializable: ^6.8.0 16 | build_runner: ^2.4.13 -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "73.0.0" 12 | _macros: 13 | dependency: transitive 14 | description: dart 15 | source: sdk 16 | version: "0.3.2" 17 | adaptive_number: 18 | dependency: transitive 19 | description: 20 | name: adaptive_number 21 | sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" 22 | url: "https://pub.dev" 23 | source: hosted 24 | version: "1.0.0" 25 | analyzer: 26 | dependency: transitive 27 | description: 28 | name: analyzer 29 | sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" 30 | url: "https://pub.dev" 31 | source: hosted 32 | version: "6.8.0" 33 | ansi_regex: 34 | dependency: transitive 35 | description: 36 | name: ansi_regex 37 | sha256: ca4f2b24a85e797a1512e1d3fe34d5f8429648f78e2268b6a8b5628c8430e643 38 | url: "https://pub.dev" 39 | source: hosted 40 | version: "0.1.2" 41 | ansi_strip: 42 | dependency: transitive 43 | description: 44 | name: ansi_strip 45 | sha256: "9bb54e10962ac1de86b9b64a278a5b8965a28a2f741975eac7fe9fb0ebe1aaac" 46 | url: "https://pub.dev" 47 | source: hosted 48 | version: "0.1.1+1" 49 | ansi_styles: 50 | dependency: transitive 51 | description: 52 | name: ansi_styles 53 | sha256: "9c656cc12b3c27b17dd982b2cc5c0cfdfbdabd7bc8f3ae5e8542d9867b47ce8a" 54 | url: "https://pub.dev" 55 | source: hosted 56 | version: "0.3.2+1" 57 | args: 58 | dependency: transitive 59 | description: 60 | name: args 61 | sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 62 | url: "https://pub.dev" 63 | source: hosted 64 | version: "2.6.0" 65 | async: 66 | dependency: transitive 67 | description: 68 | name: async 69 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 70 | url: "https://pub.dev" 71 | source: hosted 72 | version: "2.12.0" 73 | bcrypt: 74 | dependency: "direct main" 75 | description: 76 | name: bcrypt 77 | sha256: "9dc3f234d5935a76917a6056613e1a6d9b53f7fa56f98e24cd49b8969307764b" 78 | url: "https://pub.dev" 79 | source: hosted 80 | version: "1.1.3" 81 | boolean_selector: 82 | dependency: transitive 83 | description: 84 | name: boolean_selector 85 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 86 | url: "https://pub.dev" 87 | source: hosted 88 | version: "2.1.2" 89 | buffer: 90 | dependency: transitive 91 | description: 92 | name: buffer 93 | sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1" 94 | url: "https://pub.dev" 95 | source: hosted 96 | version: "1.2.3" 97 | build: 98 | dependency: transitive 99 | description: 100 | name: build 101 | sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" 102 | url: "https://pub.dev" 103 | source: hosted 104 | version: "2.4.1" 105 | build_config: 106 | dependency: transitive 107 | description: 108 | name: build_config 109 | sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 110 | url: "https://pub.dev" 111 | source: hosted 112 | version: "1.1.1" 113 | build_daemon: 114 | dependency: transitive 115 | description: 116 | name: build_daemon 117 | sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" 118 | url: "https://pub.dev" 119 | source: hosted 120 | version: "4.0.2" 121 | build_resolvers: 122 | dependency: transitive 123 | description: 124 | name: build_resolvers 125 | sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" 126 | url: "https://pub.dev" 127 | source: hosted 128 | version: "2.4.2" 129 | build_runner: 130 | dependency: "direct dev" 131 | description: 132 | name: build_runner 133 | sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" 134 | url: "https://pub.dev" 135 | source: hosted 136 | version: "2.4.13" 137 | build_runner_core: 138 | dependency: transitive 139 | description: 140 | name: build_runner_core 141 | sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 142 | url: "https://pub.dev" 143 | source: hosted 144 | version: "7.3.2" 145 | built_collection: 146 | dependency: transitive 147 | description: 148 | name: built_collection 149 | sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 150 | url: "https://pub.dev" 151 | source: hosted 152 | version: "5.1.1" 153 | built_value: 154 | dependency: transitive 155 | description: 156 | name: built_value 157 | sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb 158 | url: "https://pub.dev" 159 | source: hosted 160 | version: "8.9.2" 161 | chalkdart: 162 | dependency: transitive 163 | description: 164 | name: chalkdart 165 | sha256: "0b7ec5c6a6bafd1445500632c00c573722bd7736e491675d4ac3fe560bbd9cfe" 166 | url: "https://pub.dev" 167 | source: hosted 168 | version: "2.2.1" 169 | characters: 170 | dependency: transitive 171 | description: 172 | name: characters 173 | sha256: "81269c8d3f45541082bfbb117bbc962cfc68b5197eb4c705a00db4ddf394e1c1" 174 | url: "https://pub.dev" 175 | source: hosted 176 | version: "1.3.1" 177 | charcode: 178 | dependency: transitive 179 | description: 180 | name: charcode 181 | sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 182 | url: "https://pub.dev" 183 | source: hosted 184 | version: "1.3.1" 185 | checked_yaml: 186 | dependency: transitive 187 | description: 188 | name: checked_yaml 189 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 190 | url: "https://pub.dev" 191 | source: hosted 192 | version: "2.0.3" 193 | cli_completion: 194 | dependency: transitive 195 | description: 196 | name: cli_completion 197 | sha256: "72e8ccc4545f24efa7bbdf3bff7257dc9d62b072dee77513cc54295575bc9220" 198 | url: "https://pub.dev" 199 | source: hosted 200 | version: "0.5.1" 201 | cli_launcher: 202 | dependency: transitive 203 | description: 204 | name: cli_launcher 205 | sha256: "5e7e0282b79e8642edd6510ee468ae2976d847a0a29b3916e85f5fa1bfe24005" 206 | url: "https://pub.dev" 207 | source: hosted 208 | version: "0.3.1" 209 | cli_table: 210 | dependency: transitive 211 | description: 212 | name: cli_table 213 | sha256: "61b61c6dbfa248d8ec9c65b1d97d1ec1952482765563533087ec550405def016" 214 | url: "https://pub.dev" 215 | source: hosted 216 | version: "1.0.2" 217 | cli_util: 218 | dependency: transitive 219 | description: 220 | name: cli_util 221 | sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c 222 | url: "https://pub.dev" 223 | source: hosted 224 | version: "0.4.2" 225 | clock: 226 | dependency: transitive 227 | description: 228 | name: clock 229 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 230 | url: "https://pub.dev" 231 | source: hosted 232 | version: "1.1.2" 233 | code_builder: 234 | dependency: transitive 235 | description: 236 | name: code_builder 237 | sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" 238 | url: "https://pub.dev" 239 | source: hosted 240 | version: "4.10.1" 241 | collection: 242 | dependency: "direct main" 243 | description: 244 | name: collection 245 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 246 | url: "https://pub.dev" 247 | source: hosted 248 | version: "1.19.1" 249 | conventional_commit: 250 | dependency: transitive 251 | description: 252 | name: conventional_commit 253 | sha256: dec15ad1118f029c618651a4359eb9135d8b88f761aa24e4016d061cd45948f2 254 | url: "https://pub.dev" 255 | source: hosted 256 | version: "0.6.0+1" 257 | convert: 258 | dependency: transitive 259 | description: 260 | name: convert 261 | sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 262 | url: "https://pub.dev" 263 | source: hosted 264 | version: "3.1.2" 265 | coverage: 266 | dependency: transitive 267 | description: 268 | name: coverage 269 | sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" 270 | url: "https://pub.dev" 271 | source: hosted 272 | version: "1.10.0" 273 | crypto: 274 | dependency: transitive 275 | description: 276 | name: crypto 277 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 278 | url: "https://pub.dev" 279 | source: hosted 280 | version: "3.0.6" 281 | dart_jsonwebtoken: 282 | dependency: "direct main" 283 | description: 284 | name: dart_jsonwebtoken 285 | sha256: adf073720e491d64fa599942615b919915710af2d809b2798146f9b7c4330f3f 286 | url: "https://pub.dev" 287 | source: hosted 288 | version: "2.14.1" 289 | dart_style: 290 | dependency: transitive 291 | description: 292 | name: dart_style 293 | sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" 294 | url: "https://pub.dev" 295 | source: hosted 296 | version: "2.3.7" 297 | dotenv: 298 | dependency: transitive 299 | description: 300 | name: dotenv 301 | sha256: "379e64b6fc82d3df29461d349a1796ecd2c436c480d4653f3af6872eccbc90e1" 302 | url: "https://pub.dev" 303 | source: hosted 304 | version: "4.2.0" 305 | east_asian_width: 306 | dependency: transitive 307 | description: 308 | name: east_asian_width 309 | sha256: a13c5487dab7ddbad48875789819f0ea38a61cbaaa3024ebe7b199521e6f5788 310 | url: "https://pub.dev" 311 | source: hosted 312 | version: "1.0.1" 313 | ed25519_edwards: 314 | dependency: transitive 315 | description: 316 | name: ed25519_edwards 317 | sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" 318 | url: "https://pub.dev" 319 | source: hosted 320 | version: "0.3.1" 321 | emoji_regex: 322 | dependency: transitive 323 | description: 324 | name: emoji_regex 325 | sha256: "3a25dd4d16f98b6f76dc37cc9ae49b8511891ac4b87beac9443a1e9f4634b6c7" 326 | url: "https://pub.dev" 327 | source: hosted 328 | version: "0.0.5" 329 | equatable: 330 | dependency: transitive 331 | description: 332 | name: equatable 333 | sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 334 | url: "https://pub.dev" 335 | source: hosted 336 | version: "2.0.5" 337 | ez_validator_dart: 338 | dependency: transitive 339 | description: 340 | name: ez_validator_dart 341 | sha256: "0f00c75a35ca3ef812e19b7206a479c261f47bbdabd28d05d8dd01180b6b355b" 342 | url: "https://pub.dev" 343 | source: hosted 344 | version: "0.3.1" 345 | ffi: 346 | dependency: transitive 347 | description: 348 | name: ffi 349 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 350 | url: "https://pub.dev" 351 | source: hosted 352 | version: "2.1.3" 353 | file: 354 | dependency: transitive 355 | description: 356 | name: file 357 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 358 | url: "https://pub.dev" 359 | source: hosted 360 | version: "7.0.1" 361 | fixnum: 362 | dependency: transitive 363 | description: 364 | name: fixnum 365 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 366 | url: "https://pub.dev" 367 | source: hosted 368 | version: "1.1.1" 369 | frontend_server_client: 370 | dependency: transitive 371 | description: 372 | name: frontend_server_client 373 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 374 | url: "https://pub.dev" 375 | source: hosted 376 | version: "4.0.0" 377 | get_it: 378 | dependency: transitive 379 | description: 380 | name: get_it 381 | sha256: c49895c1ecb0ee2a0ec568d39de882e2c299ba26355aa6744ab1001f98cebd15 382 | url: "https://pub.dev" 383 | source: hosted 384 | version: "8.0.2" 385 | glob: 386 | dependency: transitive 387 | description: 388 | name: glob 389 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 390 | url: "https://pub.dev" 391 | source: hosted 392 | version: "2.1.2" 393 | grammer: 394 | dependency: transitive 395 | description: 396 | name: grammer 397 | sha256: "333c0f99fb116ae554276f64769c3a5219d314bb4fc9de43d83ae9746e0b4dd4" 398 | url: "https://pub.dev" 399 | source: hosted 400 | version: "1.0.3" 401 | graphs: 402 | dependency: transitive 403 | description: 404 | name: graphs 405 | sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" 406 | url: "https://pub.dev" 407 | source: hosted 408 | version: "2.3.2" 409 | http: 410 | dependency: "direct main" 411 | description: 412 | name: http 413 | sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 414 | url: "https://pub.dev" 415 | source: hosted 416 | version: "1.2.2" 417 | http_multi_server: 418 | dependency: transitive 419 | description: 420 | name: http_multi_server 421 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 422 | url: "https://pub.dev" 423 | source: hosted 424 | version: "3.2.1" 425 | http_parser: 426 | dependency: transitive 427 | description: 428 | name: http_parser 429 | sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" 430 | url: "https://pub.dev" 431 | source: hosted 432 | version: "4.1.1" 433 | intl: 434 | dependency: transitive 435 | description: 436 | name: intl 437 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 438 | url: "https://pub.dev" 439 | source: hosted 440 | version: "0.19.0" 441 | io: 442 | dependency: transitive 443 | description: 444 | name: io 445 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 446 | url: "https://pub.dev" 447 | source: hosted 448 | version: "1.0.4" 449 | js: 450 | dependency: transitive 451 | description: 452 | name: js 453 | sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf 454 | url: "https://pub.dev" 455 | source: hosted 456 | version: "0.7.1" 457 | json_annotation: 458 | dependency: "direct main" 459 | description: 460 | name: json_annotation 461 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 462 | url: "https://pub.dev" 463 | source: hosted 464 | version: "4.9.0" 465 | json_serializable: 466 | dependency: "direct dev" 467 | description: 468 | name: json_serializable 469 | sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b 470 | url: "https://pub.dev" 471 | source: hosted 472 | version: "6.8.0" 473 | lints: 474 | dependency: "direct dev" 475 | description: 476 | name: lints 477 | sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" 478 | url: "https://pub.dev" 479 | source: hosted 480 | version: "5.0.0" 481 | logging: 482 | dependency: "direct main" 483 | description: 484 | name: logging 485 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 486 | url: "https://pub.dev" 487 | source: hosted 488 | version: "1.3.0" 489 | macros: 490 | dependency: transitive 491 | description: 492 | name: macros 493 | sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" 494 | url: "https://pub.dev" 495 | source: hosted 496 | version: "0.1.2-main.4" 497 | mason_logger: 498 | dependency: transitive 499 | description: 500 | name: mason_logger 501 | sha256: b6d6d159927a4165f197ffc5993ea680dd41c59daf35bff23bae28390c09a36e 502 | url: "https://pub.dev" 503 | source: hosted 504 | version: "0.3.1" 505 | matcher: 506 | dependency: transitive 507 | description: 508 | name: matcher 509 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 510 | url: "https://pub.dev" 511 | source: hosted 512 | version: "0.12.16+1" 513 | melos: 514 | dependency: "direct dev" 515 | description: 516 | name: melos 517 | sha256: a62abfa8c7826cec927f8585572bb9adf591be152150494d879ca2c75118809d 518 | url: "https://pub.dev" 519 | source: hosted 520 | version: "6.2.0" 521 | meta: 522 | dependency: transitive 523 | description: 524 | name: meta 525 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 526 | url: "https://pub.dev" 527 | source: hosted 528 | version: "1.16.0" 529 | mime: 530 | dependency: "direct main" 531 | description: 532 | name: mime 533 | sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" 534 | url: "https://pub.dev" 535 | source: hosted 536 | version: "1.0.6" 537 | mustache_template: 538 | dependency: transitive 539 | description: 540 | name: mustache_template 541 | sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c 542 | url: "https://pub.dev" 543 | source: hosted 544 | version: "2.0.0" 545 | mysql_client: 546 | dependency: transitive 547 | description: 548 | name: mysql_client 549 | sha256: "6a0fdcbe3e0721c637f97ad24649be2f70dbce2b21ede8f962910e640f753fc2" 550 | url: "https://pub.dev" 551 | source: hosted 552 | version: "0.0.27" 553 | node_preamble: 554 | dependency: transitive 555 | description: 556 | name: node_preamble 557 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 558 | url: "https://pub.dev" 559 | source: hosted 560 | version: "2.0.2" 561 | package_config: 562 | dependency: transitive 563 | description: 564 | name: package_config 565 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 566 | url: "https://pub.dev" 567 | source: hosted 568 | version: "2.1.0" 569 | path: 570 | dependency: "direct main" 571 | description: 572 | name: path 573 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 574 | url: "https://pub.dev" 575 | source: hosted 576 | version: "1.9.1" 577 | pharaoh: 578 | dependency: "direct main" 579 | description: 580 | name: pharaoh 581 | sha256: b5614cef80b341bac6ffb15407c1eb446eb509a7f2d86ea7b9676ed98208adcc 582 | url: "https://pub.dev" 583 | source: hosted 584 | version: "0.0.8+2" 585 | platform: 586 | dependency: transitive 587 | description: 588 | name: platform 589 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 590 | url: "https://pub.dev" 591 | source: hosted 592 | version: "3.1.6" 593 | pointycastle: 594 | dependency: transitive 595 | description: 596 | name: pointycastle 597 | sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" 598 | url: "https://pub.dev" 599 | source: hosted 600 | version: "3.9.1" 601 | pool: 602 | dependency: transitive 603 | description: 604 | name: pool 605 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 606 | url: "https://pub.dev" 607 | source: hosted 608 | version: "1.5.1" 609 | postgres: 610 | dependency: transitive 611 | description: 612 | name: postgres 613 | sha256: c271fb05cf83f47ff8d6915ea7fc780381e581309f55846a21a3257ad6b05f6d 614 | url: "https://pub.dev" 615 | source: hosted 616 | version: "3.4.1" 617 | process: 618 | dependency: transitive 619 | description: 620 | name: process 621 | sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" 622 | url: "https://pub.dev" 623 | source: hosted 624 | version: "5.0.3" 625 | prompts: 626 | dependency: transitive 627 | description: 628 | name: prompts 629 | sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1" 630 | url: "https://pub.dev" 631 | source: hosted 632 | version: "2.0.0" 633 | pub_semver: 634 | dependency: transitive 635 | description: 636 | name: pub_semver 637 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 638 | url: "https://pub.dev" 639 | source: hosted 640 | version: "2.1.4" 641 | pub_updater: 642 | dependency: transitive 643 | description: 644 | name: pub_updater 645 | sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" 646 | url: "https://pub.dev" 647 | source: hosted 648 | version: "0.4.0" 649 | pubspec: 650 | dependency: transitive 651 | description: 652 | name: pubspec 653 | sha256: f534a50a2b4d48dc3bc0ec147c8bd7c304280fff23b153f3f11803c4d49d927e 654 | url: "https://pub.dev" 655 | source: hosted 656 | version: "2.3.0" 657 | pubspec_parse: 658 | dependency: transitive 659 | description: 660 | name: pubspec_parse 661 | sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 662 | url: "https://pub.dev" 663 | source: hosted 664 | version: "1.3.0" 665 | quiver: 666 | dependency: transitive 667 | description: 668 | name: quiver 669 | sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 670 | url: "https://pub.dev" 671 | source: hosted 672 | version: "3.2.2" 673 | recase: 674 | dependency: transitive 675 | description: 676 | name: recase 677 | sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 678 | url: "https://pub.dev" 679 | source: hosted 680 | version: "4.1.0" 681 | reflectable: 682 | dependency: transitive 683 | description: 684 | name: reflectable 685 | sha256: "35ee17c3b759fa935cc7e9247445903384520fd174e0d6c142d8288e5439fd5b" 686 | url: "https://pub.dev" 687 | source: hosted 688 | version: "4.0.12" 689 | sasl_scram: 690 | dependency: transitive 691 | description: 692 | name: sasl_scram 693 | sha256: a47207a436eb650f8fdcf54a2e2587b850dc3caef9973ce01f332b07a6fc9cb9 694 | url: "https://pub.dev" 695 | source: hosted 696 | version: "0.1.1" 697 | saslprep: 698 | dependency: transitive 699 | description: 700 | name: saslprep 701 | sha256: "3d421d10be9513bf4459c17c5e70e7b8bc718c9fc5ad4ba5eb4f5fd27396f740" 702 | url: "https://pub.dev" 703 | source: hosted 704 | version: "1.0.3" 705 | shared: 706 | dependency: "direct main" 707 | description: 708 | path: "packages/shared" 709 | relative: true 710 | source: path 711 | version: "1.0.0" 712 | shelf: 713 | dependency: transitive 714 | description: 715 | name: shelf 716 | sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 717 | url: "https://pub.dev" 718 | source: hosted 719 | version: "1.4.2" 720 | shelf_packages_handler: 721 | dependency: transitive 722 | description: 723 | name: shelf_packages_handler 724 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 725 | url: "https://pub.dev" 726 | source: hosted 727 | version: "3.0.2" 728 | shelf_static: 729 | dependency: transitive 730 | description: 731 | name: shelf_static 732 | sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 733 | url: "https://pub.dev" 734 | source: hosted 735 | version: "1.1.3" 736 | shelf_web_socket: 737 | dependency: transitive 738 | description: 739 | name: shelf_web_socket 740 | sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" 741 | url: "https://pub.dev" 742 | source: hosted 743 | version: "2.0.0" 744 | source_gen: 745 | dependency: transitive 746 | description: 747 | name: source_gen 748 | sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" 749 | url: "https://pub.dev" 750 | source: hosted 751 | version: "1.5.0" 752 | source_helper: 753 | dependency: transitive 754 | description: 755 | name: source_helper 756 | sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" 757 | url: "https://pub.dev" 758 | source: hosted 759 | version: "1.3.4" 760 | source_map_stack_trace: 761 | dependency: transitive 762 | description: 763 | name: source_map_stack_trace 764 | sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b 765 | url: "https://pub.dev" 766 | source: hosted 767 | version: "2.1.2" 768 | source_maps: 769 | dependency: transitive 770 | description: 771 | name: source_maps 772 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 773 | url: "https://pub.dev" 774 | source: hosted 775 | version: "0.10.12" 776 | source_span: 777 | dependency: transitive 778 | description: 779 | name: source_span 780 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 781 | url: "https://pub.dev" 782 | source: hosted 783 | version: "1.10.0" 784 | spanner: 785 | dependency: transitive 786 | description: 787 | name: spanner 788 | sha256: "52768be5029fb1d408e2e762cb282214b79b52c4702e1efb144df139ba40d6cd" 789 | url: "https://pub.dev" 790 | source: hosted 791 | version: "1.0.4" 792 | spookie: 793 | dependency: "direct dev" 794 | description: 795 | name: spookie 796 | sha256: cba3eec162caa861fbc2e15ef1a70abef1531286a254e7411e4f6676479bbba1 797 | url: "https://pub.dev" 798 | source: hosted 799 | version: "1.0.2+3" 800 | sprintf: 801 | dependency: transitive 802 | description: 803 | name: sprintf 804 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 805 | url: "https://pub.dev" 806 | source: hosted 807 | version: "7.0.0" 808 | sqflite_common: 809 | dependency: transitive 810 | description: 811 | name: sqflite_common 812 | sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" 813 | url: "https://pub.dev" 814 | source: hosted 815 | version: "2.5.4+5" 816 | sqflite_common_ffi: 817 | dependency: transitive 818 | description: 819 | name: sqflite_common_ffi 820 | sha256: d316908f1537725427ff2827a5c5f3b2c1bc311caed985fe3c9b10939c9e11ca 821 | url: "https://pub.dev" 822 | source: hosted 823 | version: "2.3.4" 824 | sqlite3: 825 | dependency: transitive 826 | description: 827 | name: sqlite3 828 | sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed 829 | url: "https://pub.dev" 830 | source: hosted 831 | version: "2.4.7" 832 | stack_trace: 833 | dependency: transitive 834 | description: 835 | name: stack_trace 836 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 837 | url: "https://pub.dev" 838 | source: hosted 839 | version: "1.12.0" 840 | stemmer: 841 | dependency: transitive 842 | description: 843 | name: stemmer 844 | sha256: "9a548a410ad690152b7de946c45e8b166f157f2811fb3ad717da3721f5cee144" 845 | url: "https://pub.dev" 846 | source: hosted 847 | version: "2.2.0" 848 | stream_channel: 849 | dependency: transitive 850 | description: 851 | name: stream_channel 852 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 853 | url: "https://pub.dev" 854 | source: hosted 855 | version: "2.1.2" 856 | stream_transform: 857 | dependency: transitive 858 | description: 859 | name: stream_transform 860 | sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" 861 | url: "https://pub.dev" 862 | source: hosted 863 | version: "2.1.0" 864 | string_scanner: 865 | dependency: transitive 866 | description: 867 | name: string_scanner 868 | sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" 869 | url: "https://pub.dev" 870 | source: hosted 871 | version: "1.4.0" 872 | string_width: 873 | dependency: transitive 874 | description: 875 | name: string_width 876 | sha256: "0ea481fbb6d5e2d70937fea303d8cc9296048da107dffeecf2acb675c8b47e7f" 877 | url: "https://pub.dev" 878 | source: hosted 879 | version: "0.1.5" 880 | synchronized: 881 | dependency: transitive 882 | description: 883 | name: synchronized 884 | sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" 885 | url: "https://pub.dev" 886 | source: hosted 887 | version: "3.3.0+3" 888 | term_glyph: 889 | dependency: transitive 890 | description: 891 | name: term_glyph 892 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 893 | url: "https://pub.dev" 894 | source: hosted 895 | version: "1.2.1" 896 | test: 897 | dependency: "direct dev" 898 | description: 899 | name: test 900 | sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" 901 | url: "https://pub.dev" 902 | source: hosted 903 | version: "1.25.8" 904 | test_api: 905 | dependency: transitive 906 | description: 907 | name: test_api 908 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 909 | url: "https://pub.dev" 910 | source: hosted 911 | version: "0.7.3" 912 | test_core: 913 | dependency: transitive 914 | description: 915 | name: test_core 916 | sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" 917 | url: "https://pub.dev" 918 | source: hosted 919 | version: "0.6.5" 920 | timing: 921 | dependency: transitive 922 | description: 923 | name: timing 924 | sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" 925 | url: "https://pub.dev" 926 | source: hosted 927 | version: "1.0.1" 928 | tuple: 929 | dependency: transitive 930 | description: 931 | name: tuple 932 | sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 933 | url: "https://pub.dev" 934 | source: hosted 935 | version: "2.0.2" 936 | typed_data: 937 | dependency: transitive 938 | description: 939 | name: typed_data 940 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 941 | url: "https://pub.dev" 942 | source: hosted 943 | version: "1.4.0" 944 | unorm_dart: 945 | dependency: transitive 946 | description: 947 | name: unorm_dart 948 | sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" 949 | url: "https://pub.dev" 950 | source: hosted 951 | version: "0.3.0" 952 | uri: 953 | dependency: transitive 954 | description: 955 | name: uri 956 | sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" 957 | url: "https://pub.dev" 958 | source: hosted 959 | version: "1.0.0" 960 | uuid: 961 | dependency: "direct main" 962 | description: 963 | name: uuid 964 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 965 | url: "https://pub.dev" 966 | source: hosted 967 | version: "4.5.1" 968 | vm_service: 969 | dependency: transitive 970 | description: 971 | name: vm_service 972 | sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" 973 | url: "https://pub.dev" 974 | source: hosted 975 | version: "14.3.1" 976 | watcher: 977 | dependency: transitive 978 | description: 979 | name: watcher 980 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 981 | url: "https://pub.dev" 982 | source: hosted 983 | version: "1.1.0" 984 | wcwidth: 985 | dependency: transitive 986 | description: 987 | name: wcwidth 988 | sha256: "4e68ce25701e56647cb305ab6d8c75fce5e5196227bcb6ba6886513ac36474c2" 989 | url: "https://pub.dev" 990 | source: hosted 991 | version: "0.0.4" 992 | web: 993 | dependency: transitive 994 | description: 995 | name: web 996 | sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb 997 | url: "https://pub.dev" 998 | source: hosted 999 | version: "1.1.0" 1000 | web_socket: 1001 | dependency: transitive 1002 | description: 1003 | name: web_socket 1004 | sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" 1005 | url: "https://pub.dev" 1006 | source: hosted 1007 | version: "0.1.6" 1008 | web_socket_channel: 1009 | dependency: transitive 1010 | description: 1011 | name: web_socket_channel 1012 | sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" 1013 | url: "https://pub.dev" 1014 | source: hosted 1015 | version: "3.0.1" 1016 | webkit_inspection_protocol: 1017 | dependency: transitive 1018 | description: 1019 | name: webkit_inspection_protocol 1020 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 1021 | url: "https://pub.dev" 1022 | source: hosted 1023 | version: "1.2.1" 1024 | win32: 1025 | dependency: transitive 1026 | description: 1027 | name: win32 1028 | sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" 1029 | url: "https://pub.dev" 1030 | source: hosted 1031 | version: "5.8.0" 1032 | yaml: 1033 | dependency: transitive 1034 | description: 1035 | name: yaml 1036 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 1037 | url: "https://pub.dev" 1038 | source: hosted 1039 | version: "3.1.2" 1040 | yaml_edit: 1041 | dependency: transitive 1042 | description: 1043 | name: yaml_edit 1044 | sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f 1045 | url: "https://pub.dev" 1046 | source: hosted 1047 | version: "2.2.1" 1048 | yaroorm: 1049 | dependency: "direct main" 1050 | description: 1051 | path: "." 1052 | ref: HEAD 1053 | resolved-ref: "5c543571adb8689756cc1d7154c2bf2f7fe0e820" 1054 | url: "https://github.com/codekeyz/yaroorm.git" 1055 | source: git 1056 | version: "0.0.4" 1057 | yaroorm_cli: 1058 | dependency: "direct dev" 1059 | description: 1060 | path: "packages/yaroorm_cli" 1061 | ref: HEAD 1062 | resolved-ref: "5c543571adb8689756cc1d7154c2bf2f7fe0e820" 1063 | url: "https://github.com/codekeyz/yaroorm.git" 1064 | source: git 1065 | version: "0.0.3+1" 1066 | sdks: 1067 | dart: ">=3.5.0 <4.0.0" 1068 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: backend 2 | description: Example project showcasing a full blown backend with Yaroo 3 | repository: https://github.com/codekeyz/yaroo 4 | version: 1.0.0 5 | 6 | publish_to: none 7 | 8 | environment: 9 | sdk: ^3.2.0 10 | 11 | dependencies: 12 | mime: ^1.0.4 13 | uuid: ^4.2.1 14 | path: ^1.9.0 15 | bcrypt: ^1.1.3 16 | http: ^1.1.2 17 | pharaoh: ^0.0.8+2 18 | yaroorm: 19 | git: 20 | url: 'https://github.com/codekeyz/yaroorm.git' 21 | collection: ^1.18.0 22 | json_annotation: ^4.9.0 23 | 24 | logging: ^1.3.0 25 | dart_jsonwebtoken: ^2.12.2 26 | shared: 27 | path: 'packages/shared' 28 | 29 | dev_dependencies: 30 | lints: ^5.0.0 31 | test: ^1.24.0 32 | melos: ^6.2.0 33 | build_runner: ^2.4.13 34 | spookie: 1.0.2+3 35 | json_serializable: ^6.8.0 36 | yaroorm_cli: 37 | git: 38 | url: 'https://github.com/codekeyz/yaroorm.git' 39 | path: 'packages/yaroorm_cli' 40 | -------------------------------------------------------------------------------- /pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: shared 2 | dependency_overrides: 3 | shared: 4 | path: packages/shared 5 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/dart-blog/c1c9b767ba0f549d7cf17c2d6a30c3384bf4ee4a/screenshot.png -------------------------------------------------------------------------------- /test/backend_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:backend/backend.dart'; 5 | import 'package:backend/src/models.dart'; 6 | import 'package:shared/models.dart'; 7 | import 'package:shared/shared.dart'; 8 | 9 | import 'package:spookie/spookie.dart'; 10 | import '../database/database.dart' as database; 11 | 12 | import 'backend_test.reflectable.dart'; 13 | 14 | void main() { 15 | initializeReflectable(); 16 | 17 | database.initializeORM(); 18 | 19 | late Spookie testAgent; 20 | 21 | setUpAll(() async { 22 | await blogApp.bootstrap(listen: false); 23 | testAgent = await blogApp.tester; 24 | }); 25 | 26 | group('Backend API', () { 27 | const baseAPIPath = '/api'; 28 | 29 | group('Auth', () { 30 | const authPath = '$baseAPIPath/auth'; 31 | group('.register', () { 32 | final path = '$authPath/register'; 33 | test('should error on invalid body', () async { 34 | attemptRegister(Map body, {dynamic errors}) async { 35 | return testAgent 36 | .post(path, body) 37 | .expectStatus(HttpStatus.badRequest) 38 | .expectJsonBody({'location': 'body', 'errors': errors}).test(); 39 | } 40 | 41 | // when empty body 42 | await attemptRegister({}, errors: [ 43 | 'name: The field is required', 44 | 'email: The field is required', 45 | 'password: The field is required', 46 | ]); 47 | 48 | // when only name provide 49 | await attemptRegister({ 50 | 'name': 'Foo' 51 | }, errors: [ 52 | 'email: The field is required', 53 | 'password: The field is required', 54 | ]); 55 | 56 | // when invalid email 57 | await attemptRegister({ 58 | 'name': 'Foo', 59 | 'email': 'bar' 60 | }, errors: [ 61 | 'email: The field is not a valid email address', 62 | 'password: The field is required', 63 | ]); 64 | 65 | // when no password 66 | await attemptRegister( 67 | {'name': 'Foo', 'email': 'foo@bar.com'}, 68 | errors: ['password: The field is required'], 69 | ); 70 | 71 | // when short password 72 | await attemptRegister( 73 | {'name': 'Foo', 'email': 'foo@bar.com', 'password': '344'}, 74 | errors: ['password: The field must be at least 8 characters long'], 75 | ); 76 | }); 77 | 78 | test('should create user', () async { 79 | final newUserEmail = 'foo-${DateTime.now().millisecondsSinceEpoch}@bar.com'; 80 | final apiResult = await testAgent.post(path, { 81 | 'name': 'Foo User', 82 | 'email': newUserEmail, 83 | 'password': 'foo-bar-mee-moo', 84 | }).actual; 85 | 86 | expect(apiResult.statusCode, HttpStatus.ok); 87 | expect(apiResult.headers[HttpHeaders.contentTypeHeader], 'application/json; charset=utf-8'); 88 | 89 | final user = User.fromJson(jsonDecode(apiResult.body)['user']); 90 | expect(user.email, newUserEmail); 91 | expect(user.name, 'Foo User'); 92 | expect(user.id, isNotNull); 93 | expect(user.createdAt, isNotNull); 94 | expect(user.updatedAt, isNotNull); 95 | }); 96 | 97 | test('should error on existing email', () async { 98 | final randomUser = await ServerUserQuery.findOne(); 99 | expect(randomUser, isA()); 100 | 101 | await testAgent 102 | .post(path, {'email': randomUser!.email, 'name': 'Foo Bar', 'password': 'moooasdfmdf'}) 103 | .expectStatus(HttpStatus.badRequest) 104 | .expectJsonBody({ 105 | 'errors': ['Email already taken'] 106 | }) 107 | .test(); 108 | }); 109 | }); 110 | 111 | group('.login', () { 112 | final path = '$authPath/login'; 113 | 114 | test('should error on invalid body', () async { 115 | attemptLogin(Map body, {dynamic errors}) async { 116 | return testAgent 117 | .post('$authPath/login', body) 118 | .expectStatus(HttpStatus.badRequest) 119 | .expectJsonBody({'location': 'body', 'errors': errors}).test(); 120 | } 121 | 122 | await attemptLogin({}, errors: ['email: The field is required', 'password: The field is required']); 123 | await attemptLogin({'email': 'foo-bar@hello.com'}, errors: ['password: The field is required']); 124 | await attemptLogin( 125 | {'email': 'foo-bar'}, 126 | errors: ['email: The field is not a valid email address', 'password: The field is required'], 127 | ); 128 | }); 129 | 130 | test('should error on in-valid credentials', () async { 131 | final randomUser = await ServerUserQuery.findOne(); 132 | expect(randomUser, isA()); 133 | 134 | final email = randomUser!.email; 135 | 136 | await testAgent 137 | .post(path, {'email': email, 'password': 'wap wap wap'}) 138 | .expectStatus(HttpStatus.unauthorized) 139 | .expectJsonBody({ 140 | 'errors': ['Email or Password not valid'] 141 | }) 142 | .test(); 143 | 144 | await testAgent 145 | .post(path, {'email': 'holy@bar.com', 'password': 'wap wap wap'}) 146 | .expectStatus(HttpStatus.unauthorized) 147 | .expectJsonBody({ 148 | 'errors': ['Email or Password not valid'] 149 | }) 150 | .test(); 151 | }); 152 | 153 | test('should success on valid credentials', () async { 154 | final randomUser = await ServerUserQuery.findOne(); 155 | expect(randomUser, isA()); 156 | 157 | final baseTest = testAgent.post(path, { 158 | 'email': randomUser!.email, 159 | 'password': 'foo-bar-mee-moo', 160 | }); 161 | 162 | await baseTest 163 | .expectStatus(HttpStatus.ok) 164 | .expectJsonBody({'user': randomUser.toJson()}) 165 | .expectHeader(HttpHeaders.setCookieHeader, contains('auth=s%')) 166 | .test(); 167 | }); 168 | }); 169 | }); 170 | 171 | group('', () { 172 | String? authCookie; 173 | ServerUser? currentUser; 174 | 175 | setUpAll(() async { 176 | currentUser = await ServerUserQuery.findOne(); 177 | expect(currentUser, isA()); 178 | 179 | final result = await testAgent.post('$baseAPIPath/auth/login', { 180 | 'email': currentUser!.email, 181 | 'password': 'foo-bar-mee-moo', 182 | }).actual; 183 | 184 | authCookie = result.headers[HttpHeaders.setCookieHeader]; 185 | expect(authCookie, isNotNull); 186 | 187 | await currentUser!.articles.delete(); 188 | }); 189 | 190 | group('Users', () { 191 | final usersApiPath = '$baseAPIPath/users'; 192 | 193 | test('should reject if no cookie', () async { 194 | await testAgent 195 | .get(usersApiPath) 196 | .expectStatus(HttpStatus.unauthorized) 197 | .expectJsonBody({'error': 'Unauthorized'}).test(); 198 | }); 199 | 200 | test('should return user for /users/me ', () async { 201 | await testAgent 202 | .get('$usersApiPath/me', headers: {HttpHeaders.cookieHeader: authCookie!}) 203 | .expectStatus(HttpStatus.ok) 204 | .expectHeader(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8') 205 | .expectBodyCustom( 206 | (body) => User.fromJson(jsonDecode(body)['user']), 207 | isA(), 208 | ) 209 | .test(); 210 | }); 211 | }); 212 | 213 | group('Articles', () { 214 | final articleApiPath = '$baseAPIPath/articles'; 215 | 216 | group('create', () { 217 | test('should error on invalid body', () async { 218 | Future attemptCreate(Map body, {dynamic errors}) async { 219 | return testAgent 220 | .post(articleApiPath, body, headers: {HttpHeaders.cookieHeader: authCookie!}) 221 | .expectStatus(HttpStatus.badRequest) 222 | .expectJsonBody({'location': 'body', 'errors': errors}) 223 | .test(); 224 | } 225 | 226 | // when empty body 227 | await attemptCreate({}, errors: ['title: The field is required', 'description: The field is required']); 228 | 229 | // when short title or description 230 | await attemptCreate({ 231 | 'title': 'a', 232 | 'description': 'df' 233 | }, errors: [ 234 | 'title: The field must be at least 5 characters long', 235 | 'description: The field must be at least 10 characters long' 236 | ]); 237 | }); 238 | 239 | test('should create with image', () async { 240 | final result = await testAgent.post(articleApiPath, { 241 | 'title': 'Santa Clause 🚀', 242 | 'description': 'Dart for backend is here', 243 | 'imageUrl': 'https://holy-dart.com/dart-logo-for-shares.png' 244 | }, headers: { 245 | HttpHeaders.cookieHeader: authCookie! 246 | }).actual; 247 | expect(result.statusCode, HttpStatus.ok); 248 | 249 | final article = Article.fromJson(jsonDecode(result.body)['article']); 250 | expect(article.ownerId, currentUser!.id); 251 | expect(article.title, 'Santa Clause 🚀'); 252 | expect(article.description, 'Dart for backend is here'); 253 | expect(article.imageUrl, 'https://holy-dart.com/dart-logo-for-shares.png'); 254 | expect(article.id, isNotNull); 255 | expect(article.createdAt, isNotNull); 256 | expect(article.updatedAt, isNotNull); 257 | }); 258 | 259 | test('should use default image if none set', () async { 260 | final result = await testAgent.post( 261 | articleApiPath, 262 | {'title': 'Hurry 🚀', 'description': 'Welcome to the jungle'}, 263 | headers: {HttpHeaders.cookieHeader: authCookie!}, 264 | ).actual; 265 | expect(result.statusCode, HttpStatus.ok); 266 | 267 | final article = Article.fromJson(jsonDecode(result.body)['article']); 268 | expect(article.ownerId, currentUser!.id); 269 | expect(article.title, 'Hurry 🚀'); 270 | expect(article.description, 'Welcome to the jungle'); 271 | expect(article.imageUrl, defaultArticleImage); 272 | expect(article.id, isNotNull); 273 | expect(article.createdAt, isNotNull); 274 | expect(article.updatedAt, isNotNull); 275 | }); 276 | }); 277 | 278 | group('update ', () { 279 | test('should error when invalid params', () async { 280 | // bad params 281 | await testAgent 282 | .put('$articleApiPath/some-random-id', headers: {HttpHeaders.cookieHeader: authCookie!}) 283 | .expectStatus(HttpStatus.badRequest) 284 | .expectJsonBody({ 285 | 'location': 'param', 286 | 'errors': ['articleId must be a int type'] 287 | }) 288 | .test(); 289 | 290 | // bad body 291 | await testAgent 292 | .put('$articleApiPath/234', body: {'name': 'foo'}, headers: {HttpHeaders.cookieHeader: authCookie!}) 293 | .expectStatus(HttpStatus.badRequest) 294 | .expectJsonBody({ 295 | 'location': 'body', 296 | 'errors': ['title: The field is required', 'description: The field is required'] 297 | }) 298 | .test(); 299 | 300 | // no existing article 301 | await testAgent 302 | .put( 303 | '$articleApiPath/234', 304 | body: {'title': 'Honey', 'description': 'Hold my beer lets talk'}, 305 | headers: {HttpHeaders.cookieHeader: authCookie!}, 306 | ) 307 | .expectStatus(HttpStatus.notFound) 308 | .expectJsonBody({'error': 'Not found'}) 309 | .test(); 310 | }); 311 | 312 | test('should update article', () async { 313 | final article = await ServerArticleQuery.where((article) => article.ownerId(currentUser!.id)).findOne(); 314 | expect(article, isA
()); 315 | 316 | expect(article!.title, isNot('Honey')); 317 | expect(article.description, isNot('Hold my beer lets talk')); 318 | 319 | final result = await testAgent.put( 320 | '$articleApiPath/${article.id}', 321 | body: {'title': 'Honey', 'description': 'Hold my beer lets talk'}, 322 | headers: {HttpHeaders.cookieHeader: authCookie!}, 323 | ).actual; 324 | expect(result.statusCode, HttpStatus.ok); 325 | 326 | final updatedArticle = Article.fromJson(jsonDecode(result.body)['article']); 327 | expect(updatedArticle.title, 'Honey'); 328 | expect(updatedArticle.description, 'Hold my beer lets talk'); 329 | expect(updatedArticle.toJson(), allOf(contains('id'), contains('created_at'), contains('updated_at'))); 330 | }); 331 | }); 332 | 333 | group('delete', () { 334 | test('should error when invalid params', () async { 335 | // bad params 336 | await testAgent 337 | .delete('$articleApiPath/some-random-id', headers: {HttpHeaders.cookieHeader: authCookie!}) 338 | .expectStatus(HttpStatus.badRequest) 339 | .expectJsonBody({ 340 | 'location': 'param', 341 | 'errors': ['articleId must be a int type'] 342 | }) 343 | .test(); 344 | 345 | const fakeId = 234239389239; 346 | final article = await ServerArticleQuery.findById(fakeId); 347 | expect(article, isNull); 348 | 349 | await testAgent 350 | .delete('$articleApiPath/$fakeId', headers: {HttpHeaders.cookieHeader: authCookie!}) 351 | .expectStatus(HttpStatus.ok) 352 | .expectJsonBody({'message': 'Article deleted'}) 353 | .test(); 354 | }); 355 | 356 | test('should delete article', () async { 357 | final article = await ServerArticleQuery.findByOwnerId(currentUser!.id); 358 | expect(article, isA
()); 359 | 360 | await testAgent 361 | .delete('$articleApiPath/${article!.id}', headers: {HttpHeaders.cookieHeader: authCookie!}) 362 | .expectStatus(HttpStatus.ok) 363 | .expectJsonBody({'message': 'Article deleted'}) 364 | .test(); 365 | 366 | expect(await ServerArticleQuery.findById(article.id), isNull); 367 | }); 368 | }); 369 | 370 | group('when get article by Id', () { 371 | test('should error when invalid articleId', () async { 372 | await testAgent 373 | .get('$articleApiPath/some-random-id', headers: {HttpHeaders.cookieHeader: authCookie!}) 374 | .expectStatus(HttpStatus.badRequest) 375 | .expectJsonBody({ 376 | 'location': 'param', 377 | 'errors': ['articleId must be a int type'] 378 | }) 379 | .test(); 380 | }); 381 | 382 | test('should error when article not exist', () async { 383 | await testAgent 384 | .get('$articleApiPath/2348', headers: {HttpHeaders.cookieHeader: authCookie!}) 385 | .expectStatus(HttpStatus.notFound) 386 | .expectJsonBody({'error': 'Not found'}) 387 | .test(); 388 | }); 389 | 390 | test('should show article without auth', () async { 391 | final article = await ServerArticleQuery.findByOwnerId(currentUser!.id); 392 | expect(article, isA
()); 393 | 394 | await testAgent.get('$articleApiPath/${article!.id}').expectStatus(HttpStatus.ok).expectJsonBody({ 395 | 'article': { 396 | ...article.toJson(), 397 | 'author': currentUser!.toJson(), 398 | } 399 | }).test(); 400 | }); 401 | }); 402 | 403 | test('should get Articles without auth', () async { 404 | final articles = await ServerArticleQuery.findMany(); 405 | expect(articles, isNotEmpty); 406 | 407 | await testAgent 408 | .get('$baseAPIPath/articles') 409 | .expectStatus(HttpStatus.ok) 410 | .expectHeader(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8') 411 | .expectBodyCustom( 412 | (body) { 413 | final result = jsonDecode(body)['articles'] as Iterable; 414 | return result.map((e) => Article.fromJson(e)).toList(); 415 | }, 416 | hasLength(articles.length), 417 | ).test(); 418 | }); 419 | }); 420 | }); 421 | }); 422 | } 423 | --------------------------------------------------------------------------------