From ed863ecb4150ac1ab164089a4d1c306c9c934b18 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 17 Dec 2021 18:24:32 +0800 Subject: [PATCH] feat: change api framework to angel3. --- api/.dockerignore | 10 + api/.gitignore | 99 +++++- api/AUTHORS.md | 12 + api/CHANGELOG.md | 9 + api/CONTRIBUTING.md | 24 ++ api/Dockerfile | 14 + api/LICENSE | 29 ++ api/README.md | 71 ++-- api/analysis_options.yaml | 98 +----- api/bin/dev.dart | 28 ++ api/bin/main.dart | 13 - api/bin/migrate.dart | 28 ++ api/bin/prod.dart | 27 ++ api/config.src.yaml | 6 - api/config/default.yaml | 12 + api/lib/channel.dart | 77 ----- api/lib/dde_gesture_manager.dart | 11 - api/lib/dde_gesture_manager_api.dart | 17 + api/lib/model/model.dart | 18 - api/lib/models.dart | 1 + api/lib/src/config/config.dart | 30 ++ api/lib/src/config/plugins/orm.dart | 35 ++ api/lib/src/config/plugins/plugins.dart | 8 + api/lib/src/models/base_model.dart | 8 + api/lib/src/models/user.dart | 19 ++ api/lib/src/models/user.g.dart | 369 +++++++++++++++++++++ .../routes/controllers/controller_extensions.dart | 21 ++ .../src/routes/controllers/user_controllers.dart | 16 + api/lib/src/routes/routes.dart | 39 +++ api/pubspec.yaml | 41 ++- api/test/all_test.dart | 43 +++ api/test/harness/app.dart | 38 --- api/test/simple_controller_test.dart | 32 -- api/views/error.html | 5 + api/views/layout.html | 10 + api/web/css/site.css | 27 ++ api/web/images/favicon.png | Bin 0 -> 10624 bytes api/web/login.html | 29 -- api/web/robots.txt | 2 + 39 files changed, 1008 insertions(+), 368 deletions(-) create mode 100644 api/.dockerignore create mode 100644 api/AUTHORS.md create mode 100644 api/CHANGELOG.md create mode 100644 api/CONTRIBUTING.md create mode 100644 api/Dockerfile create mode 100644 api/LICENSE create mode 100644 api/bin/dev.dart delete mode 100644 api/bin/main.dart create mode 100644 api/bin/migrate.dart create mode 100644 api/bin/prod.dart delete mode 100644 api/config.src.yaml create mode 100644 api/config/default.yaml delete mode 100644 api/lib/channel.dart delete mode 100644 api/lib/dde_gesture_manager.dart create mode 100644 api/lib/dde_gesture_manager_api.dart delete mode 100644 api/lib/model/model.dart create mode 100644 api/lib/models.dart create mode 100644 api/lib/src/config/config.dart create mode 100644 api/lib/src/config/plugins/orm.dart create mode 100644 api/lib/src/config/plugins/plugins.dart create mode 100644 api/lib/src/models/base_model.dart create mode 100644 api/lib/src/models/user.dart create mode 100644 api/lib/src/models/user.g.dart create mode 100644 api/lib/src/routes/controllers/controller_extensions.dart create mode 100644 api/lib/src/routes/controllers/user_controllers.dart create mode 100644 api/lib/src/routes/routes.dart create mode 100644 api/test/all_test.dart delete mode 100644 api/test/harness/app.dart delete mode 100644 api/test/simple_controller_test.dart create mode 100644 api/views/error.html create mode 100644 api/views/layout.html create mode 100644 api/web/css/site.css create mode 100644 api/web/images/favicon.png delete mode 100644 api/web/login.html create mode 100644 api/web/robots.txt diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..faf23bd --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,10 @@ +.dart_tool +.idea +.pub +.vscode +logs/ +test/ +build/ +.analysis-options +.packages +*.g.dart \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index 57ad605..a0541d9 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,28 +1,95 @@ # Created by .ignore support plugin (hsz.mobi) ### Dart template -# See https://www.dartlang.org/guides/libraries/private-files +# See https://www.dartlang.org/tools/private-files.html + +# source_gen +.dart_tool # Files and directories created by pub -.dart_tool/ +.buildlog .packages +.project +.pub/ +.scripts-bin/ build/ -# If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock - -# Directory created by dartdoc -# If you don't generate documentation locally you can remove this line. -doc/api/ +**/packages/ -# Avoid committing generated Javascript files: +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) *.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. -*.js_ +*.part.js *.js.deps *.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### VSCode template +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +logs/ +*.pem +.DS_Store +server_log.txt -.flutter-plugins -.flutter-plugins-dependencies +.metals/ -config.yaml \ No newline at end of file +/config/production.yaml +/config/development.yaml diff --git a/api/AUTHORS.md b/api/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/api/AUTHORS.md @@ -0,0 +1,12 @@ +Primary Authors +=============== + +* __[Thomas Hii](dukefirehawk.apps@gmail.com)__ + + Thomas is the current maintainer of the code base. He has refactored and migrated the + code base to support NNBD. + +* __[Tobe O](thosakwe@gmail.com)__ + + Tobe has written much of the original code prior to NNBD migration. He has moved on and + is no longer involved with the project. diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md new file mode 100644 index 0000000..6c9985b --- /dev/null +++ b/api/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +## 1.0.0 + +* Changed to use `angel3` packages +* Updated to support NNBD +* Updated README +* Updated default `postgresql` setup +* Updated linter to `package:lints` diff --git a/api/CONTRIBUTING.md b/api/CONTRIBUTING.md new file mode 100644 index 0000000..b34fbca --- /dev/null +++ b/api/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contribution + +Any help from the open-source community is always welcome and needed: + +1. Found an issue? + - Please [fill a bug report][tracker] with error message and steps to reproduce it. +2. Wish a feature? + - Open a feature request with use cases. +3. Are you using and liking the project? + - Create an article about your use case + - Do a post on your likes and dislikes + - Make a donation. +4. Are you a developer? + - Fix a bug and send a [pull request][pull_request] + - Implement a new feature + - Improve the Unit Tests + - Improve the [User Guide][doc] and send a [document pull request][doc_repo] +5. Have you already helped in any way? + - **Many thanks to the contributors and everybody that uses this project!** + +[tracker]: https://github.com/dukefirehawk/angel/issues +[pull_request]: https://github.com/dukefirehawk/angel/pulls +[doc]: https://angel3-docs.dukefirehawk.com +[doc_repo]: https://github.com/dukefirehawk/angel3-guide/pulls \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..c84bc87 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,14 @@ +FROM google/dart:latest + +COPY ./ ./ + +# Install dependencies, pre-build +RUN pub get + +# Optionally build generaed sources. +# RUN pub run build_runner build + +# Set environment, start server +ENV ANGEL_ENV=production +EXPOSE 3000 +CMD dart bin/prod.dart diff --git a/api/LICENSE b/api/LICENSE new file mode 100644 index 0000000..df5e063 --- /dev/null +++ b/api/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, dukefirehawk.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/api/README.md b/api/README.md index 7849780..2088a61 100644 --- a/api/README.md +++ b/api/README.md @@ -1,37 +1,62 @@ -# dde_gesture_manager +# ORM Starter Application for Angel3 framework -## Running the Application Locally +This is an ORM starter application for [Angel3 framework](https://angel3-framework.web.app) which is a full-stack Web framework in Dart. The default database is `postgresql`. `mysql` support is still in active development. -Run `conduit serve` from this directory to run the application. For running within an IDE, run `bin/main.dart`. By default, a configuration file named `config.yaml` will be used. +## Installation & Setup -You must have a `config.yaml` file that has correct database connection info, which should point to a local database. To configure a database to match your application's schema, run the following commands: +1. Download and install [Dart](https://dart.dev/get-dart). +2. Install `postgresql` version 9, 10, 11 or 12. **postgresql 13 is not working as the driver do not support SCRAM** +3. Create a new user and database in postgres using `psql` cli. For example: -``` -# if this is a project, run db generate first -conduit db generate -conduit db upgrade --connect postgres://user:password@localhost:5432/app_name -``` + ```sql + postgres=# create database appdb; + postgres=# create user appuser with encrypted password 'App1970#'; + postgres=# grant all privileges on database appdb to appuser; + ``` -To generate a SwaggerUI client, run `conduit document client`. +4. Update the `postgres` section in the `config/default.yaml` file with the newly created user and database name. -## Running Application Tests + ```yaml + postgres: + host: localhost + port: 5432 + database_name: appdb + username: appuser + password: App1970# + useSSL: false + time_zone: UTC + ``` -Tests are run with a local PostgreSQL database named `conduit_test_db`. If this database does not exist, create it from your SQL prompt: +5. Run the migration to generate `migrations` and `greetings` tables in the database. -CREATE DATABASE conduit_test_db; -CREATE USER conduit_test_user WITH createdb; -ALTER USER conduit_test_user WITH password 'conduit!'; -GRANT all ON DATABASE conduit_test_db TO conduit_test_user; + ```bash + dart bin/migration.dart + ``` +### Development -To run all tests for this application, run the following in this directory: +1. Run the following command to start Angel3 server in dev mode to *hot-reloaded* on file changes: -``` -pub run test -``` + ```bash + dart --observe bin/dev.dart + ``` -The default configuration file used when testing is `config.src.yaml`. This file should be checked into version control. It also the template for configuration files used in deployment. +2. Modify the code and watch the changes applied to the application -## Deploying an Application +### Production -See the documentation for [Deployment](https://conduit.io/docs/deploy/). \ No newline at end of file +1. Run the following command: + + ```bash + dart bin/prod.dart + ``` + +2. Run as docker. Edit and run the provided `Dockerfile` to build the image. + +## Resources + +Visit the [Developer Guide](https://angel3-docs.dukefirehawk.com/guides) for dozens of guides and resources, including video tutorials, to get up and running as quickly as possible with Angel3. + +Examples and complete projects can be found [here](https://angel3-framework.web.app/#/examples). + +You can also view the [API Documentation](https://pub.dev/documentation/angel3_framework/latest/). diff --git a/api/analysis_options.yaml b/api/analysis_options.yaml index d474280..d66fc45 100644 --- a/api/analysis_options.yaml +++ b/api/analysis_options.yaml @@ -1,98 +1,6 @@ -analyzer: - strong-mode: - implicit-casts: false +include: package:lints/recommended.yaml linter: rules: - - always_declare_return_types - - always_put_control_body_on_new_line - - always_put_required_named_parameters_first - - always_require_non_null_named_parameters - - annotate_overrides - - avoid_bool_literals_in_conditional_expressions - - avoid_double_and_int_checks - - avoid_empty_else - - avoid_field_initializers_in_const_classes - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_positional_boolean_parameters - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_types_as_parameter_names - - avoid_unused_constructor_parameters - - await_only_futures - - camel_case_types - - cancel_subscriptions - - close_sinks - - comment_references - - constant_identifier_names - - control_flow_in_finally - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - join_return_with_assignment - - library_names - - library_prefixes - - list_remove_unrelated_type - - literal_only_boolean_expressions - - no_duplicate_case_values - - non_constant_identifier_names - - null_closures - - package_api_docs - - package_names - - package_prefixed_library_names - - parameter_assignments - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_constructors_over_static_methods - - prefer_contains - - prefer_equal_for_default_values - - prefer_final_fields - - prefer_final_locals - - prefer_foreach - - prefer_generic_function_type_aliases - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_iterable_whereType - - prefer_typing_uninitialized_variables - - recursive_getters - - slash_for_doc_comments - - sort_constructors_first - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - type_annotate_public_apis - - type_init_formals - - unawaited_futures - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_in_if_null_operators - - unnecessary_overrides - - unnecessary_parenthesis - - unnecessary_statements - - unnecessary_this - - unrelated_type_equality_checks - - use_rethrow_when_possible - - use_string_buffers - - use_to_and_as_if_applicable - - valid_regexps - - void_checks \ No newline at end of file + avoid_renaming_method_parameters: false + overridden_fields: false diff --git a/api/bin/dev.dart b/api/bin/dev.dart new file mode 100644 index 0000000..05028ae --- /dev/null +++ b/api/bin/dev.dart @@ -0,0 +1,28 @@ +import 'dart:io'; +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:belatuk_pretty_logging/belatuk_pretty_logging.dart'; +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_hot/angel3_hot.dart'; +import 'package:logging/logging.dart'; + +void main() async { + // Watch the config/ and web/ directories for changes, and hot-reload the server. + hierarchicalLoggingEnabled = true; + + var hot = HotReloader(() async { + var logger = Logger.detached('dde_gesture_manager_api') + ..level = Level.ALL + ..onRecord.listen(prettyLog); + var app = Angel(logger: logger, reflector: MirrorsReflector()); + await app.configure(configureServer); + return app; + }, [ + Directory('config'), + Directory('lib'), + ]); + + var server = await hot.startServer('127.0.0.1', 3000); + print( + 'dde_gesture_manager_api server listening at http://${server.address.address}:${server.port}'); +} diff --git a/api/bin/main.dart b/api/bin/main.dart deleted file mode 100644 index 37d4c6c..0000000 --- a/api/bin/main.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -Future main() async { - final app = Application() - ..options.configurationFilePath = "config.yaml" - ..options.port = 8888; - - await app.startOnCurrentIsolate(); - - print("Application started on port: ${app.options.port}."); - print("Click to open in browser: http://localhost:${app.options.port}"); - print("Use Ctrl-C (SIGINT) to stop running the application."); -} diff --git a/api/bin/migrate.dart b/api/bin/migrate.dart new file mode 100644 index 0000000..5c5e413 --- /dev/null +++ b/api/bin/migrate.dart @@ -0,0 +1,28 @@ +import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_migration_runner/angel3_migration_runner.dart'; +import 'package:angel3_migration_runner/postgres.dart'; +import 'package:file/local.dart'; +import 'package:logging/logging.dart'; + +void main(List args) async { + // Enable the logging + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((rec) { + print('${rec.time}: ${rec.level.name}: ${rec.loggerName}: ${rec.message}'); + + if (rec.error != null) { + print(rec.error); + print(rec.stackTrace); + } + }); + + var fs = LocalFileSystem(); + var configuration = await loadStandaloneConfiguration(fs); + var connection = await connectToPostgres(configuration); + var migrationRunner = PostgresMigrationRunner(connection, migrations: [ + UserMigration(), + ]); + await runMigrations(migrationRunner, args); +} diff --git a/api/bin/prod.dart b/api/bin/prod.dart new file mode 100644 index 0000000..3a4437b --- /dev/null +++ b/api/bin/prod.dart @@ -0,0 +1,27 @@ +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:angel3_production/angel3_production.dart'; + +// NOTE: By default, the Runner class does not use the `MirrorsReflector`, or any +// reflector, by default. +// +// If your application is using any sort of functionality reliant on annotations or reflection, +// either include the MirrorsReflector, or use a static reflector variant. +// +// The following use cases require reflection: +// * Use of Controllers, via @Expose() or @ExposeWS() +// * Use of dependency injection into constructors, whether in controllers or plain `container.make` calls +// * Use of the `ioc` function in any route +// +// The `MirrorsReflector` from `package:angel_container/mirrors.dart` is by far the most convenient pattern, +// so use it if possible. +// +// However, the following alternatives exist: +// * Generation via `package:angel_container_generator` +// * Creating an instance of `StaticReflector` +// * Manually implementing the `Reflector` interface (cumbersome; not recommended) +// +// As of January 4th, 2018, the documentation has not yet been updated to state this, +// so in the meantime, visit the Angel chat for further questions: +// +// https://gitter.im/angel_dart/discussion +void main(List args) => Runner('dde_gesture_manager_api', configureServer).run(args); diff --git a/api/config.src.yaml b/api/config.src.yaml deleted file mode 100644 index d3edf9a..0000000 --- a/api/config.src.yaml +++ /dev/null @@ -1,6 +0,0 @@ -database: - username: conduit_test_user - password: conduit! - host: localhost - port: 15432 # change this value - databaseName: conduit_test_db diff --git a/api/config/default.yaml b/api/config/default.yaml new file mode 100644 index 0000000..16dd769 --- /dev/null +++ b/api/config/default.yaml @@ -0,0 +1,12 @@ +# Default server configuration. +host: 127.0.0.1 +port: 3000 +postgres: + host: localhost + port: 5432 + database_name: appdb + username: appuser + password: App1970# + useSSL: false + time_zone: UTC +jwt_secret: "OvA9SBLnncot8gFHvt8Gh1qkQ1ptGIQW" \ No newline at end of file diff --git a/api/lib/channel.dart b/api/lib/channel.dart deleted file mode 100644 index 2eeca45..0000000 --- a/api/lib/channel.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; -import 'package:dde_gesture_manager/model/model.dart'; - -/// This type initializes an application. -/// -/// Override methods in this class to set up routes and initialize services like -/// database connections. See http://conduit.io/docs/http/channel/. -class DdeGestureManagerChannel extends ApplicationChannel { - late ManagedContext context; - - /// Initialize services in this method. - /// - /// Implement this method to initialize services, read values from [options] - /// and any other initialization required before constructing [entryPoint]. - /// - /// This method is invoked prior to [entryPoint] being accessed. - @override - Future prepare() async { - logger.onRecord.listen( - (rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}")); - - final config = - DdeGestureManagerConfiguration(options!.configurationFilePath!); - context = contextWithConnectionInfo(config.database!); - } - - /// Construct the request channel. - /// - /// Return an instance of some [Controller] that will be the initial receiver - /// of all [Request]s. - /// - /// This method is invoked after [prepare]. - @override - Controller get entryPoint { - final router = Router(); - - router - .route("/model/[:id]") - .link(() => ManagedObjectController(context)); - - router - .route("/") - .linkFunction((Request request) async => Response.ok('ok')); - - return router; - } - - /* - * Helper methods - */ - - ManagedContext contextWithConnectionInfo( - DatabaseConfiguration connectionInfo) { - final dataModel = ManagedDataModel.fromCurrentMirrorSystem(); - final psc = PostgreSQLPersistentStore( - connectionInfo.username, - connectionInfo.password, - connectionInfo.host, - connectionInfo.port, - connectionInfo.databaseName); - - return ManagedContext(dataModel, psc); - } -} - -/// An instance of this class reads values from a configuration -/// file specific to this application. -/// -/// Configuration files must have key-value for the properties in this class. -/// For more documentation on configuration files, see https://conduit.io/docs/configure/ and -/// https://pub.dartlang.org/packages/safe_config. -class DdeGestureManagerConfiguration extends Configuration { - DdeGestureManagerConfiguration(String fileName) - : super.fromFile(File(fileName)); - - DatabaseConfiguration? database; -} diff --git a/api/lib/dde_gesture_manager.dart b/api/lib/dde_gesture_manager.dart deleted file mode 100644 index 713096c..0000000 --- a/api/lib/dde_gesture_manager.dart +++ /dev/null @@ -1,11 +0,0 @@ -/// dde_gesture_manager -/// -/// A conduit web server. -library dde_gesture_manager; - -export 'dart:async'; -export 'dart:io'; - -export 'package:conduit/conduit.dart'; - -export 'channel.dart'; diff --git a/api/lib/dde_gesture_manager_api.dart b/api/lib/dde_gesture_manager_api.dart new file mode 100644 index 0000000..aa2f208 --- /dev/null +++ b/api/lib/dde_gesture_manager_api.dart @@ -0,0 +1,17 @@ +/// Your very own web application! +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:file/local.dart'; +import 'src/config/config.dart' as configuration; +import 'src/routes/routes.dart' as routes; + +/// Configures the server instance. +Future configureServer(Angel app) async { + // Grab a handle to the file system, so that we can do things like + // serve static files. + var fs = const LocalFileSystem(); + + // Set up our application, using the plug-ins defined with this project. + await app.configure(configuration.configureServer(fs)); + await app.configure(routes.configureServer(fs)); +} diff --git a/api/lib/model/model.dart b/api/lib/model/model.dart deleted file mode 100644 index 6eb8a1b..0000000 --- a/api/lib/model/model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -class Model extends ManagedObject<_Model> implements _Model { - @override - void willInsert() { - createdAt = DateTime.now().toUtc(); - } -} - -class _Model { - @primaryKey - int? id; - - @Column(indexed: true) - String? name; - - DateTime? createdAt; -} diff --git a/api/lib/models.dart b/api/lib/models.dart new file mode 100644 index 0000000..b72d675 --- /dev/null +++ b/api/lib/models.dart @@ -0,0 +1 @@ +export 'src/models/user.dart'; diff --git a/api/lib/src/config/config.dart b/api/lib/src/config/config.dart new file mode 100644 index 0000000..358f42e --- /dev/null +++ b/api/lib/src/config/config.dart @@ -0,0 +1,30 @@ +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_jinja/angel3_jinja.dart'; +import 'package:file/file.dart'; +import 'plugins/plugins.dart' as plugins; + +/// This is a perfect place to include configuration and load plug-ins. +AngelConfigurer configureServer(FileSystem fileSystem) { + return (Angel app) async { + // Load configuration from the `config/` directory. + // + // See: https://github.com/angel-dart/configuration + await app.configure(configuration(fileSystem)); + + // Configure our application to render jinja templates from the `views/` directory. + // + // See: https://github.com/angel-dart/jinja + await app.configure(jinja(path: fileSystem.directory('views').path)); + + // Apply another plug-ins, i.e. ones that *you* have written. + // + // Typically, the plugins in `lib/src/config/plugins/plugins.dart` are plug-ins + // that add functionality specific to your application. + // + // If you write a plug-in that you plan to use again, or are + // using one created by the community, include it in + // `lib/src/config/config.dart`. + await plugins.configureServer(app); + }; +} diff --git a/api/lib/src/config/plugins/orm.dart b/api/lib/src/config/plugins/orm.dart new file mode 100644 index 0000000..be286f8 --- /dev/null +++ b/api/lib/src/config/plugins/orm.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_orm_postgres/angel3_orm_postgres.dart'; +import 'package:postgres/postgres.dart'; + +Future configureServer(Angel app) async { + var connection = await connectToPostgres(app.configuration); + await connection.open(); + + var logger = app.environment.isProduction ? null : app.logger; + var executor = PostgreSqlExecutor(connection, logger: logger); + + app + ..container!.registerSingleton(executor) + ..shutdownHooks.add((_) => connection.close()); +} + +Future connectToPostgres(Map configuration) async { + var postgresConfig = configuration['postgres'] as Map? ?? {}; + var connection = PostgreSQLConnection( + postgresConfig['host'] as String? ?? 'localhost', + postgresConfig['port'] as int? ?? 5432, + postgresConfig['database_name'] as String? ?? + Platform.environment['USER'] ?? + Platform.environment['USERNAME'] ?? + '', + username: postgresConfig['username'] as String?, + password: postgresConfig['password'] as String?, + timeZone: postgresConfig['time_zone'] as String? ?? 'UTC', + timeoutInSeconds: postgresConfig['timeout_in_seconds'] as int? ?? 30, + useSSL: postgresConfig['use_ssl'] as bool? ?? false); + return connection; +} diff --git a/api/lib/src/config/plugins/plugins.dart b/api/lib/src/config/plugins/plugins.dart new file mode 100644 index 0000000..acda711 --- /dev/null +++ b/api/lib/src/config/plugins/plugins.dart @@ -0,0 +1,8 @@ +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'orm.dart' as orm; + +Future configureServer(Angel app) async { + // Include any plugins you have made here. + await app.configure(orm.configureServer); +} diff --git a/api/lib/src/models/base_model.dart b/api/lib/src/models/base_model.dart new file mode 100644 index 0000000..4704517 --- /dev/null +++ b/api/lib/src/models/base_model.dart @@ -0,0 +1,8 @@ +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; + +abstract class BaseModel extends Model { + @SerializableField(isNullable: true) + @Column(type: ColumnType.json) + Map? get metadata; +} diff --git a/api/lib/src/models/user.dart b/api/lib/src/models/user.dart new file mode 100644 index 0000000..1288a20 --- /dev/null +++ b/api/lib/src/models/user.dart @@ -0,0 +1,19 @@ +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:dde_gesture_manager_api/src/models/base_model.dart'; +import 'package:optional/optional.dart'; +part 'user.g.dart'; + +@serializable +@orm +abstract class _User extends BaseModel { + @SerializableField(isNullable: false) + String? get email; + + @SerializableField(isNullable: false) + String? get password; + + @SerializableField(isNullable: false) + String? get token; +} \ No newline at end of file diff --git a/api/lib/src/models/user.g.dart b/api/lib/src/models/user.g.dart new file mode 100644 index 0000000..669e5f8 --- /dev/null +++ b/api/lib/src/models/user.g.dart @@ -0,0 +1,369 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// MigrationGenerator +// ************************************************************************** + +class UserMigration extends Migration { + @override + void up(Schema schema) { + schema.create('users', (table) { + table.serial('id').primaryKey(); + table.timeStamp('created_at'); + table.timeStamp('updated_at'); + table.declareColumn( + 'metadata', Column(type: ColumnType('jsonb'), length: 256)); + table.varChar('email', length: 256); + table.varChar('password', length: 256); + table.varChar('token', length: 256); + }); + } + + @override + void down(Schema schema) { + schema.drop('users'); + } +} + +// ************************************************************************** +// OrmGenerator +// ************************************************************************** + +class UserQuery extends Query { + UserQuery({Query? parent, Set? trampoline}) : super(parent: parent) { + trampoline ??= {}; + trampoline.add(tableName); + _where = UserQueryWhere(this); + } + + @override + final UserQueryValues values = UserQueryValues(); + + UserQueryWhere? _where; + + @override + Map get casts { + return {}; + } + + @override + String get tableName { + return 'users'; + } + + @override + List get fields { + return const [ + 'id', + 'created_at', + 'updated_at', + 'metadata', + 'email', + 'password', + 'token' + ]; + } + + @override + UserQueryWhere? get where { + return _where; + } + + @override + UserQueryWhere newWhereClause() { + return UserQueryWhere(this); + } + + static User? parseRow(List row) { + if (row.every((x) => x == null)) { + return null; + } + var model = User( + id: row[0].toString(), + createdAt: (row[1] as DateTime?), + updatedAt: (row[2] as DateTime?), + metadata: (row[3] as Map?), + email: (row[4] as String?), + password: (row[5] as String?), + token: (row[6] as String?)); + return model; + } + + @override + Optional deserialize(List row) { + return Optional.ofNullable(parseRow(row)); + } +} + +class UserQueryWhere extends QueryWhere { + UserQueryWhere(UserQuery query) + : id = NumericSqlExpressionBuilder(query, 'id'), + createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'), + updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at'), + metadata = MapSqlExpressionBuilder(query, 'metadata'), + email = StringSqlExpressionBuilder(query, 'email'), + password = StringSqlExpressionBuilder(query, 'password'), + token = StringSqlExpressionBuilder(query, 'token'); + + final NumericSqlExpressionBuilder id; + + final DateTimeSqlExpressionBuilder createdAt; + + final DateTimeSqlExpressionBuilder updatedAt; + + final MapSqlExpressionBuilder metadata; + + final StringSqlExpressionBuilder email; + + final StringSqlExpressionBuilder password; + + final StringSqlExpressionBuilder token; + + @override + List get expressionBuilders { + return [id, createdAt, updatedAt, metadata, email, password, token]; + } +} + +class UserQueryValues extends MapQueryValues { + @override + Map get casts { + return {}; + } + + String? get id { + return (values['id'] as String?); + } + + set id(String? value) => values['id'] = value; + DateTime? get createdAt { + return (values['created_at'] as DateTime?); + } + + set createdAt(DateTime? value) => values['created_at'] = value; + DateTime? get updatedAt { + return (values['updated_at'] as DateTime?); + } + + set updatedAt(DateTime? value) => values['updated_at'] = value; + Map? get metadata { + return (values['metadata'] as Map?); + } + + set metadata(Map? value) => values['metadata'] = value; + String? get email { + return (values['email'] as String?); + } + + set email(String? value) => values['email'] = value; + String? get password { + return (values['password'] as String?); + } + + set password(String? value) => values['password'] = value; + String? get token { + return (values['token'] as String?); + } + + set token(String? value) => values['token'] = value; + void copyFrom(User model) { + createdAt = model.createdAt; + updatedAt = model.updatedAt; + metadata = model.metadata; + email = model.email; + password = model.password; + token = model.token; + } +} + +// ************************************************************************** +// JsonModelGenerator +// ************************************************************************** + +@generatedSerializable +class User extends _User { + User( + {this.id, + this.createdAt, + this.updatedAt, + Map? metadata, + required this.email, + required this.password, + required this.token}) + : metadata = Map.unmodifiable(metadata ?? {}); + + /// A unique identifier corresponding to this item. + @override + String? id; + + /// The time at which this item was created. + @override + DateTime? createdAt; + + /// The last time at which this item was updated. + @override + DateTime? updatedAt; + + @override + Map? metadata; + + @override + String? email; + + @override + String? password; + + @override + String? token; + + User copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Map? metadata, + String? email, + String? password, + String? token}) { + return User( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + metadata: metadata ?? this.metadata, + email: email ?? this.email, + password: password ?? this.password, + token: token ?? this.token); + } + + @override + bool operator ==(other) { + return other is _User && + other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + MapEquality( + keys: DefaultEquality(), values: DefaultEquality()) + .equals(other.metadata, metadata) && + other.email == email && + other.password == password && + other.token == token; + } + + @override + int get hashCode { + return hashObjects( + [id, createdAt, updatedAt, metadata, email, password, token]); + } + + @override + String toString() { + return 'User(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, metadata=$metadata, email=$email, password=$password, token=$token)'; + } + + Map toJson() { + return UserSerializer.toMap(this); + } +} + +// ************************************************************************** +// SerializerGenerator +// ************************************************************************** + +const UserSerializer userSerializer = UserSerializer(); + +class UserEncoder extends Converter { + const UserEncoder(); + + @override + Map convert(User model) => UserSerializer.toMap(model); +} + +class UserDecoder extends Converter { + const UserDecoder(); + + @override + User convert(Map map) => UserSerializer.fromMap(map); +} + +class UserSerializer extends Codec { + const UserSerializer(); + + @override + UserEncoder get encoder => const UserEncoder(); + @override + UserDecoder get decoder => const UserDecoder(); + static User fromMap(Map map) { + if (map['email'] == null) { + throw FormatException("Missing required field 'email' on User."); + } + + if (map['password'] == null) { + throw FormatException("Missing required field 'password' on User."); + } + + if (map['token'] == null) { + throw FormatException("Missing required field 'token' on User."); + } + + return User( + id: map['id'] as String?, + createdAt: map['created_at'] != null + ? (map['created_at'] is DateTime + ? (map['created_at'] as DateTime) + : DateTime.parse(map['created_at'].toString())) + : null, + updatedAt: map['updated_at'] != null + ? (map['updated_at'] is DateTime + ? (map['updated_at'] as DateTime) + : DateTime.parse(map['updated_at'].toString())) + : null, + metadata: map['metadata'] is Map + ? (map['metadata'] as Map).cast() + : {}, + email: map['email'] as String?, + password: map['password'] as String?, + token: map['token'] as String?); + } + + static Map toMap(_User? model) { + if (model == null) { + return {}; + } + return { + 'id': model.id, + 'created_at': model.createdAt?.toIso8601String(), + 'updated_at': model.updatedAt?.toIso8601String(), + 'metadata': model.metadata, + 'email': model.email, + 'password': model.password, + 'token': model.token + }; + } +} + +abstract class UserFields { + static const List allFields = [ + id, + createdAt, + updatedAt, + metadata, + email, + password, + token + ]; + + static const String id = 'id'; + + static const String createdAt = 'created_at'; + + static const String updatedAt = 'updated_at'; + + static const String metadata = 'metadata'; + + static const String email = 'email'; + + static const String password = 'password'; + + static const String token = 'token'; +} diff --git a/api/lib/src/routes/controllers/controller_extensions.dart b/api/lib/src/routes/controllers/controller_extensions.dart new file mode 100644 index 0000000..f0f6007 --- /dev/null +++ b/api/lib/src/routes/controllers/controller_extensions.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_orm/angel3_orm.dart' as orm; + +extension ResponseNoContent on ResponseContext { + noContent() { + statusCode = HttpStatus.noContent; + return close(); + } +} + +extension QueryWhereId on orm.Query { + set whereId(int id) { + (where as dynamic).id.equals(id); + } +} + +extension QueryExecutor on RequestContext { + orm.QueryExecutor get queryExecutor => container!.make(); +} diff --git a/api/lib/src/routes/controllers/user_controllers.dart b/api/lib/src/routes/controllers/user_controllers.dart new file mode 100644 index 0000000..d98b1e3 --- /dev/null +++ b/api/lib/src/routes/controllers/user_controllers.dart @@ -0,0 +1,16 @@ +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'controller_extensions.dart'; + +Future configureServer(Angel app) async { + app.get( + '/user/int:id', + (req, res) async { + var user = await (UserQuery()..where?.metadata.contains({"uid": req.params[UserFields.id]})) + .getOne(req.queryExecutor); + + return res.json(user.value); + }, + ); +} diff --git a/api/lib/src/routes/routes.dart b/api/lib/src/routes/routes.dart new file mode 100644 index 0000000..23a8dec --- /dev/null +++ b/api/lib/src/routes/routes.dart @@ -0,0 +1,39 @@ +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:file/file.dart'; +import 'controllers/user_controllers.dart' as UserControllers; + +/// Put your app routes here! +/// +/// See the wiki for information about routing, requests, and responses: +/// * https://angel3-docs.dukefirehawk.com/guides/basic-routing +/// * https://angel3-docs.dukefirehawk.com/guides/requests-and-responses + +AngelConfigurer configureServer(FileSystem fileSystem) { + return (Angel app) async { + // Typically, you want to mount controllers first, after any global middleware. + await app.configure(UserControllers.configureServer); + + // Throw a 404 if no route matched the request. + app.fallback((req, res) => throw AngelHttpException.notFound()); + + // Set our application up to handle different errors. + // + // Read the following for documentation: + // * https://angel3-docs.dukefirehawk.com/guides/error-handling + + var oldErrorHandler = app.errorHandler; + app.errorHandler = (e, req, res) async { + if (req.accepts('text/html', strict: true)) { + if (e.statusCode == 404 && req.accepts('text/html', strict: true)) { + await res.render('error.html', {'message': 'No router exists for ${req.uri}'}); + } else { + return await res.render('error.html', { + 'message': [e.message, '', e.stackTrace.toString().replaceAll('\n', '
')].join('
') + }); + } + } else { + return await oldErrorHandler(e, req, res); + } + }; + }; +} diff --git a/api/pubspec.yaml b/api/pubspec.yaml index c56bbff..877dd7b 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -1,13 +1,34 @@ -name: dde_gesture_manager -description: An conduit application with a database connection and data model. -version: 0.0.1 - +name: dde_gesture_manager_api +version: 1.0.0 +description: An ORM starter application for Angel3 framework +publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - + sdk: '>=2.12.0 <3.0.0' dependencies: - conduit: ^3.0.0 - + angel3_auth: ^4.0.0 + angel3_configuration: ^4.1.0 + angel3_framework: ^4.2.0 + angel3_migration: ^4.0.0 + angel3_orm: ^4.0.0 + angel3_orm_postgres: ^3.0.0 + angel3_serialize: ^4.1.0 + angel3_production: ^3.1.0 + angel3_static: ^4.1.0 + angel3_validate: ^4.0.0 + belatuk_pretty_logging: ^4.0.0 + optional: ^6.0.0 + logging: ^1.0.0 dev_dependencies: - test: ^1.16.5 - conduit_test: ^3.0.0 \ No newline at end of file + angel3_hot: ^4.2.0 + angel3_jinja: ^2.0.1 + angel3_migration_runner: ^4.0.0 + angel3_orm_generator: ^4.1.0 + angel3_serialize_generator: ^4.2.0 + angel3_test: ^4.0.0 + build_runner: ^2.0.3 + io: ^1.0.0 + test: ^1.17.5 + lints: ^1.0.0 + + + diff --git a/api/test/all_test.dart b/api/test/all_test.dart new file mode 100644 index 0000000..cfb69fc --- /dev/null +++ b/api/test/all_test.dart @@ -0,0 +1,43 @@ +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_test/angel3_test.dart'; +import 'package:test/test.dart'; + +// Angel also includes facilities to make testing easier. +// +// `package:angel_test` ships a client that can test +// both plain HTTP and WebSockets. +// +// Tests do not require your server to actually be mounted on a port, +// so they will run faster than they would in other frameworks, where you +// would have to first bind a socket, and then account for network latency. +// +// See the documentation here: +// https://github.com/angel-dart/test +// +// If you are unfamiliar with Dart's advanced testing library, you can read up +// here: +// https://github.com/dart-lang/test + +void main() async { + late TestClient client; + + setUp(() async { + var app = Angel(); + await app.configure(configureServer); + + client = await connectTo(app); + }); + + tearDown(() async { + await client.close(); + }); + + test('index returns 200', () async { + // Request a resource at the given path. + var response = await client.get(Uri.parse('/')); + + // Expect a 200 response. + expect(response, hasStatus(200)); + }); +} diff --git a/api/test/harness/app.dart b/api/test/harness/app.dart deleted file mode 100644 index a78b76f..0000000 --- a/api/test/harness/app.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:conduit_test/conduit_test.dart'; -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -export 'package:conduit/conduit.dart'; -export 'package:conduit_test/conduit_test.dart'; -export 'package:dde_gesture_manager/dde_gesture_manager.dart'; -export 'package:test/test.dart'; - -/// A testing harness for dde_gesture_manager. -/// -/// A harness for testing an conduit application. Example test file: -/// -/// void main() { -/// Harness harness = Harness()..install(); -/// -/// test("GET /path returns 200", () async { -/// final response = await harness.agent.get("/path"); -/// expectResponse(response, 200); -/// }); -/// } -/// -class Harness extends TestHarness with TestHarnessORMMixin { - @override - ManagedContext? get context => channel?.context; - - @override - Future onSetUp() async { - await resetData(); - } - - @override - Future onTearDown() async {} - - @override - Future seed() async { - // restore any static data. called by resetData. - } -} diff --git a/api/test/simple_controller_test.dart b/api/test/simple_controller_test.dart deleted file mode 100644 index c2c0c9c..0000000 --- a/api/test/simple_controller_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'harness/app.dart'; - -Future main() async { - final harness = Harness()..install(); - - tearDown(() async { - await harness.resetData(); - }); - - test("POST /model", () async { - final response = await harness.agent!.post("/model", body: {"name": "Bob"}); - expect( - response, - hasResponse(200, - body: {"id": isNotNull, "name": "Bob", "createdAt": isTimestamp})); - }); - - test("GET /model/:id returns previously created object", () async { - var response = await harness.agent!.post("/model", body: {"name": "Bob"}); - - final createdObject = response?.body.as(); - response = - await harness.agent!.request("/model/${createdObject["id"]}").get(); - expect( - response, - hasResponse(200, body: { - "id": createdObject["id"], - "name": createdObject["name"], - "createdAt": createdObject["createdAt"] - })); - }); -} diff --git a/api/views/error.html b/api/views/error.html new file mode 100644 index 0000000..5b94687 --- /dev/null +++ b/api/views/error.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} +{% block title %}Error{% endblock %} +{% block body %} +

Error: {{ message }}

+{% endblock %} \ No newline at end of file diff --git a/api/views/layout.html b/api/views/layout.html new file mode 100644 index 0000000..2217499 --- /dev/null +++ b/api/views/layout.html @@ -0,0 +1,10 @@ + + + + + {% block title %}{% endblock %} + + + {% block body %}{% endblock %} + + \ No newline at end of file diff --git a/api/web/css/site.css b/api/web/css/site.css new file mode 100644 index 0000000..9e40b8d --- /dev/null +++ b/api/web/css/site.css @@ -0,0 +1,27 @@ +html, body { + height: 100%; +} + +body { + margin: 0; + padding: 0; + width: 100%; + display: table; + font-weight: 100; + font-family: 'Lato', sans-serif; +} + +.container { + text-align: center; + display: table-cell; + vertical-align: middle; +} + +.content { + text-align: center; + display: inline-block; +} + +.title { + font-size: 96px; +} \ No newline at end of file diff --git a/api/web/images/favicon.png b/api/web/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b8f5e2cfb5f71b9edd3358c54fc55ae965bd6366 GIT binary patch literal 10624 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 am;M`@6j>Vt*#+MK0000 - - - - - Login - - - -
-

Login

-
- - - -
- - -
-
- - -
- -
-
- - - \ No newline at end of file diff --git a/api/web/robots.txt b/api/web/robots.txt new file mode 100644 index 0000000..f328961 --- /dev/null +++ b/api/web/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /admin \ No newline at end of file