├── .gitignore ├── .hound.yml ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Dockerfile ├── Dockerfile.dev ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── Rakefile ├── app ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ └── notifications_channel.rb ├── controllers │ ├── admin │ │ ├── application_controller.rb │ │ ├── terms_of_services_controller.rb │ │ ├── user_sessions_controller.rb │ │ └── users_controller.rb │ ├── api │ │ ├── projects_controller.rb │ │ ├── tasks_controller.rb │ │ ├── terms_of_services_controller.rb │ │ ├── users │ │ │ ├── activation_emails_controller.rb │ │ │ ├── activations_controller.rb │ │ │ ├── authorizations_controller.rb │ │ │ ├── features_controller.rb │ │ │ ├── password_resets_controller.rb │ │ │ ├── passwords_controller.rb │ │ │ ├── projects_controller.rb │ │ │ └── tasks_controller.rb │ │ ├── users_controller.rb │ │ └── welcome_controller.rb │ ├── api_controller.rb │ ├── application_controller.rb │ └── concerns │ │ └── .keep ├── dashboards │ ├── terms_of_service_dashboard.rb │ └── user_dashboard.rb ├── errors │ └── api_errors.rb ├── jobs │ └── application_job.rb ├── lib │ ├── core_extensions │ │ └── action_controller │ │ │ └── resource_parameter_missing.rb │ ├── flipper_migration.rb │ └── json_web_token.rb ├── mailers │ ├── application_mailer.rb │ └── user_mailer.rb ├── models │ ├── application_record.rb │ ├── concerns │ │ ├── .keep │ │ ├── project_lifecycle.rb │ │ ├── state_machine.rb │ │ └── task_lifecycle.rb │ ├── flipper_feature.rb │ ├── flipper_gate.rb │ ├── project.rb │ ├── task.rb │ ├── terms_of_service.rb │ └── user.rb └── views │ ├── admin │ ├── application │ │ └── _navigation.html.erb │ └── user_sessions │ │ └── new.html.erb │ ├── api │ ├── errors.jbuilder │ ├── projects │ │ ├── _project.jbuilder │ │ ├── show.jbuilder │ │ ├── update.jbuilder │ │ └── update_state.jbuilder │ ├── tasks │ │ ├── _task.jbuilder │ │ ├── show.jbuilder │ │ ├── update.jbuilder │ │ ├── update_order.jbuilder │ │ └── update_state.jbuilder │ ├── terms_of_services │ │ └── current.jbuilder │ ├── users │ │ ├── _user.jbuilder │ │ ├── accept_tos.jbuilder │ │ ├── activations │ │ │ └── create.jbuilder │ │ ├── authorizations │ │ │ └── create.jbuilder │ │ ├── create.jbuilder │ │ ├── features │ │ │ └── index.jbuilder │ │ ├── passwords │ │ │ └── create.jbuilder │ │ ├── projects │ │ │ ├── create.jbuilder │ │ │ └── index.jbuilder │ │ ├── show.jbuilder │ │ ├── tasks │ │ │ ├── create.jbuilder │ │ │ └── index.jbuilder │ │ └── update.jbuilder │ └── welcome │ │ └── index.jbuilder │ ├── layouts │ ├── mailer.html.erb │ └── mailer.text.erb │ └── user_mailer │ ├── activation_needed_email.text.erb │ ├── activation_success_email.text.erb │ └── reset_password_email.text.erb ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring └── update ├── client ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .scss.yml ├── build │ ├── build.js │ ├── check-versions.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ ├── webpack.prod.conf.js │ └── webpack.test.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package-lock.json ├── package.json ├── spec │ ├── components │ │ └── Ly │ │ │ ├── LyBadge.spec.js │ │ │ ├── LyButton.spec.js │ │ │ └── LyCard.spec.js │ ├── jest.conf.js │ └── setup.js ├── src │ ├── api │ │ ├── features.js │ │ ├── http.js │ │ ├── projects.js │ │ ├── root.js │ │ ├── tasks.js │ │ ├── terms_of_services.js │ │ └── users.js │ ├── auth.js │ ├── components │ │ ├── App │ │ │ ├── App.vue │ │ │ ├── AppHeader.vue │ │ │ ├── AppLayout.vue │ │ │ ├── AppLogo.vue │ │ │ ├── AppMenu.vue │ │ │ ├── AppMenuLink.vue │ │ │ └── AppPage.vue │ │ ├── Ly │ │ │ ├── LyBadge.vue │ │ │ ├── LyButton.vue │ │ │ ├── LyCard.vue │ │ │ ├── LyCardDeck.vue │ │ │ ├── LyColumns │ │ │ │ ├── LyColumn.vue │ │ │ │ ├── LyColumns.vue │ │ │ │ └── index.js │ │ │ ├── LyForm │ │ │ │ ├── LyForm.vue │ │ │ │ ├── LyFormGroup.vue │ │ │ │ ├── LyFormInput.vue │ │ │ │ ├── LyFormSelect.vue │ │ │ │ ├── LyFormTextarea.vue │ │ │ │ └── index.js │ │ │ ├── LyIcon.vue │ │ │ ├── LyList │ │ │ │ ├── LyList.vue │ │ │ │ ├── LyListGroup.vue │ │ │ │ ├── LyListItem.vue │ │ │ │ ├── LyListItemAdapt.vue │ │ │ │ └── index.js │ │ │ ├── LyModal.vue │ │ │ ├── LyPopover │ │ │ │ ├── LyPopover.vue │ │ │ │ ├── LyPopoverItem.vue │ │ │ │ ├── LyPopoverSeparator.vue │ │ │ │ └── index.js │ │ │ ├── LySection.vue │ │ │ └── LyTextContainer.vue │ │ ├── dashboard │ │ │ └── DashboardPage.vue │ │ ├── design │ │ │ ├── DesignColorScheme.vue │ │ │ ├── DesignColorsPage.vue │ │ │ ├── DesignComponentsPage.vue │ │ │ ├── DesignGridPage.vue │ │ │ ├── DesignIndexPage.vue │ │ │ ├── DesignLogoPage.vue │ │ │ ├── DesignTypographyPage.vue │ │ │ ├── DesignVisualsPage.vue │ │ │ └── DesignWordingPage.vue │ │ ├── general │ │ │ ├── HomePage.vue │ │ │ ├── LoadingPage.vue │ │ │ ├── NotFoundPage.vue │ │ │ ├── SudoModal.vue │ │ │ ├── TermsOfServiceModal.vue │ │ │ └── TermsOfServicePage.vue │ │ ├── layouts │ │ │ ├── LayoutApplication.vue │ │ │ ├── LayoutDefault.vue │ │ │ ├── LayoutDesign.vue │ │ │ ├── LayoutEmpty.vue │ │ │ ├── LayoutProfile.vue │ │ │ └── LayoutSingleForm.vue │ │ ├── mixins │ │ │ ├── ErrorsHandler.js │ │ │ └── ResourcesLoader.js │ │ ├── onboarding │ │ │ └── OnboardingPage.vue │ │ ├── profile │ │ │ ├── ProfileDeleteAccount.vue │ │ │ ├── ProfileIdentityEditForm.vue │ │ │ ├── ProfileLanguageForm.vue │ │ │ ├── ProfilePage.vue │ │ │ ├── ProfilePasswordNewForm.vue │ │ │ └── ProfileTimeZoneForm.vue │ │ ├── projects │ │ │ ├── ProjectCard.vue │ │ │ ├── ProjectCardDeck.vue │ │ │ ├── ProjectContainer.vue │ │ │ ├── ProjectCreateForm.vue │ │ │ ├── ProjectDeleteModal.vue │ │ │ ├── ProjectEditDueDateForm.vue │ │ │ ├── ProjectEditDueDateModal.vue │ │ │ ├── ProjectEditForm.vue │ │ │ ├── ProjectEditPage.vue │ │ │ ├── ProjectFinishForm.vue │ │ │ ├── ProjectFinishModal.vue │ │ │ ├── ProjectItem.vue │ │ │ ├── ProjectItemFinished.vue │ │ │ ├── ProjectShowPage.vue │ │ │ ├── ProjectStartForm.vue │ │ │ ├── ProjectStartModal.vue │ │ │ ├── ProjectTimeline.vue │ │ │ ├── ProjectsHeader.vue │ │ │ ├── ProjectsPage.vue │ │ │ ├── ProjectsStartNewForm.vue │ │ │ └── ProjectsStartNewModal.vue │ │ ├── tasks │ │ │ ├── TaskAttachProjectForm.vue │ │ │ ├── TaskAttachProjectModal.vue │ │ │ ├── TaskConfirmAbandonModal.vue │ │ │ ├── TaskCreateForm.vue │ │ │ ├── TaskEditForm.vue │ │ │ ├── TaskIndicators.vue │ │ │ ├── TaskItem.vue │ │ │ ├── TaskList.vue │ │ │ ├── TaskSelectList.vue │ │ │ ├── TaskSelectableItem.vue │ │ │ ├── TaskTransformInProjectModal.vue │ │ │ ├── TasksChart.vue │ │ │ ├── TasksCompleteDay.vue │ │ │ ├── TasksPage.vue │ │ │ ├── TasksPlanModal.vue │ │ │ └── TasksPlanner.vue │ │ ├── today │ │ │ └── TodayPage.vue │ │ └── users │ │ │ ├── UserActivateForm.vue │ │ │ ├── UserActivatePage.vue │ │ │ ├── UserLoginForm.vue │ │ │ ├── UserLoginPage.vue │ │ │ ├── UserPasswordNewForm.vue │ │ │ ├── UserPasswordNewPage.vue │ │ │ ├── UserPasswordResetForm.vue │ │ │ ├── UserPasswordResetPage.vue │ │ │ ├── UserPopover.vue │ │ │ └── UserRegisterForm.vue │ ├── locales │ │ ├── en │ │ │ ├── formats.js │ │ │ ├── index.js │ │ │ └── messages.js │ │ ├── fr │ │ │ ├── formats.js │ │ │ ├── index.js │ │ │ └── messages.js │ │ └── index.js │ ├── main.js │ ├── router.js │ ├── store │ │ ├── index.js │ │ ├── modules │ │ │ ├── features.js │ │ │ ├── global.js │ │ │ ├── projects.js │ │ │ ├── tasks.js │ │ │ ├── terms_of_services.js │ │ │ └── users.js │ │ └── plugins │ │ │ └── cable.js │ ├── styles │ │ ├── _fonts.css │ │ ├── _forms.scss │ │ ├── _grid.scss │ │ ├── _links.scss │ │ ├── _typography.scss │ │ ├── app.scss │ │ ├── components │ │ │ └── _vue_tooltip.scss │ │ └── variables │ │ │ ├── _dimensions.scss │ │ │ ├── _index.scss │ │ │ └── colors.json │ └── utils │ │ ├── array.js │ │ ├── color.js │ │ └── object.js └── static │ ├── .gitkeep │ ├── back-home.png │ ├── favicon.ico │ ├── font-awesome-4.7.0 │ ├── HELP-US-OUT.txt │ ├── css │ │ └── font-awesome.min.css │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── icon-256.png │ ├── illustrations │ ├── colors.jpg │ ├── components.jpg │ ├── grid.jpg │ ├── logo.png │ ├── typography.jpg │ ├── visuals.jpg │ └── wording.jpg │ ├── logo-black.svg │ ├── logo-inverse.svg │ ├── logo-text-inverse.png │ ├── logo-text.png │ ├── logo-text.svg │ ├── logo.svg │ └── permanentmarker-regular.woff2 ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── core_extensions.rb │ ├── cors.rb │ ├── filter_parameter_logging.rb │ ├── flipper.rb │ ├── inflections.rb │ ├── json_param_key_transform.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ ├── sorcery.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20161226223150_sorcery_core.rb │ ├── 20161226223151_sorcery_user_activation.rb │ ├── 20161228214904_add_username_to_user.rb │ ├── 20170109212722_create_projects.rb │ ├── 20170113231928_add_description_to_project.rb │ ├── 20170114113847_add_index_on_project_name.rb │ ├── 20170115112412_add_dates_to_project.rb │ ├── 20170116065712_add_finished_at_to_project.rb │ ├── 20170116212138_add_stopped_at_to_project.rb │ ├── 20170201210318_create_tasks.rb │ ├── 20170529210621_add_abandoned_at_to_task.rb │ ├── 20170531195512_add_restarted_count_to_task.rb │ ├── 20170601094145_add_order_to_task.rb │ ├── 20170716102855_add_project_references_to_task.rb │ ├── 20171001201530_rename_tasks_restarted_count_in_started_count.rb │ ├── 20171006170245_add_state_to_project.rb │ ├── 20171006180251_rename_project_stopped_at_in_paused_at.rb │ ├── 20171006212950_add_state_to_task.rb │ ├── 20171007004320_set_project_state.rb │ ├── 20171224225137_update_task_state_on_not_started_projects.rb │ ├── 20171226101352_add_slug_to_project.rb │ ├── 20180128172912_add_admin_to_users.rb │ ├── 20180205083244_create_flipper_tables.rb │ ├── 20180617164753_create_feature_registration_feature_flag.rb │ ├── 20180628165422_create_terms_of_services.rb │ ├── 20180629162317_add_terms_of_service_reference_to_users.rb │ ├── 20180901081437_sorcery_reset_password.rb │ └── 20190513154840_add_time_zone_to_users.rb ├── schema.rb └── seeds.rb ├── docker-compose-dev.yml ├── docs ├── api │ ├── authorizations.md │ ├── errors.md │ ├── index.md │ ├── projects.md │ ├── root.md │ ├── tasks.md │ ├── terms_of_service.md │ ├── users.md │ └── websocket.md ├── backend │ ├── endpoints_design.md │ ├── index.md │ ├── lifecycle_and_state_machine.md │ └── writing_tests.md ├── development_environment.md ├── feature_flags.md ├── frontend │ ├── index.md │ └── writing_tests.md ├── index.md ├── production_environment.md ├── pull_request.md ├── pull_request_template.md ├── release.md ├── screenshots │ └── dashboard.png └── tests.md ├── lib └── tasks │ └── .keep ├── log └── .keep ├── public └── robots.txt ├── spec ├── factories │ ├── .keep │ ├── projects.rb │ ├── tasks.rb │ ├── terms_of_service.rb │ └── user.rb ├── json_web_token_spec.rb ├── mailers │ └── user_mailer_spec.rb ├── models │ ├── project_spec.rb │ ├── task_spec.rb │ ├── terms_of_service_spec.rb │ └── user_spec.rb ├── rails_helper.rb ├── requests │ └── api │ │ ├── projects_request_spec.rb │ │ ├── tasks_request_spec.rb │ │ ├── terms_of_services_request_spec.rb │ │ ├── users │ │ ├── activation_emails_request_spec.rb │ │ ├── activations_request_spec.rb │ │ ├── authorizations_request_spec.rb │ │ ├── features_request_spec.rb │ │ ├── password_resets_request_spec.rb │ │ ├── passwords_request_spec.rb │ │ ├── projects_request_spec.rb │ │ └── tasks_request_spec.rb │ │ ├── users_request_spec.rb │ │ └── welcome_request_spec.rb ├── shared_examples_for_failures.rb ├── spec_helper.rb └── support │ ├── api │ └── schemas │ │ ├── errors.json │ │ ├── features │ │ └── index.json │ │ ├── projects │ │ ├── create.json │ │ ├── index.json │ │ ├── project.json │ │ ├── show.json │ │ ├── update.json │ │ └── update_state.json │ │ ├── tasks │ │ ├── create.json │ │ ├── index.json │ │ ├── show.json │ │ ├── task.json │ │ ├── update.json │ │ ├── update_order.json │ │ └── update_state.json │ │ ├── terms_of_services │ │ └── current.json │ │ ├── users │ │ ├── accept_tos.json │ │ ├── activations │ │ │ └── create.json │ │ ├── authorizations │ │ │ └── create.json │ │ ├── create.json │ │ ├── me.json │ │ ├── passwords │ │ │ └── create.json │ │ ├── update.json │ │ └── user.json │ │ └── welcome │ │ └── index.json │ ├── factory_bot.rb │ └── json_matchers.rb └── tmp └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore Byebug command history file. 21 | .byebug_history 22 | 23 | # Ignore public/ folder 24 | public/index.html 25 | public/static 26 | 27 | /vendor 28 | 29 | client/spec/coverage/ 30 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | ruby: 2 | config_file: .rubocop.yml 3 | eslint: 4 | enabled: true 5 | config_file: client/.eslintrc 6 | jshint: 7 | enabled: false 8 | scss: 9 | enabled: true 10 | config_file: client/.scss.yml 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Rails: 2 | Enabled: true 3 | 4 | AllCops: 5 | Exclude: 6 | - 'db/schema.rb' 7 | - 'bin/*' 8 | - 'vendor/*' 9 | 10 | Metrics/LineLength: 11 | Max: 110 12 | IgnoreCopDirectives: true 13 | 14 | Documentation: 15 | Enabled: false 16 | 17 | Metrics/BlockLength: 18 | Exclude: 19 | - 'spec/**/*' 20 | 21 | Rails/DynamicFindBy: 22 | Whitelist: 23 | - find_by_authorization_token 24 | - find_by_identifier! 25 | - find_by_sorcery_token! 26 | 27 | Style/ClassAndModuleChildren: 28 | EnforcedStyle: compact 29 | 30 | Style/EmptyMethod: 31 | EnforcedStyle: expanded 32 | 33 | Style/RedundantSelf: 34 | Enabled: false 35 | 36 | Style/TrailingCommaInArguments: 37 | EnforcedStyleForMultiline: comma 38 | 39 | Style/TrailingCommaInArrayLiteral: 40 | EnforcedStyleForMultiline: comma 41 | 42 | Style/TrailingCommaInHashLiteral: 43 | EnforcedStyleForMultiline: comma 44 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4 4 | 5 | notifications: 6 | email: false 7 | 8 | cache: 9 | bundler: true 10 | npm: true 11 | 12 | services: 13 | - postgresql 14 | 15 | install: 16 | - bundle install 17 | - nvm install 10 18 | - cd client && npm cache verify && npm install && cd - 19 | 20 | before_script: 21 | - psql -c 'create database lessy_test;' -U postgres 22 | 23 | script: 24 | - bundle exec rspec spec 25 | - cd client && npm run test && cd - 26 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Lessy's contributors 2 | 3 | This document is here to thank all people who helped Lessy to be what it is. We 4 | didn't set any rules yet in order to appear here but it will definitely not be 5 | bound to code contributions. A user making feedbacks and/or answering to other 6 | users' questions is as valuable as someone making code. Don't hesitate to ask 7 | if you think you deserve to appear in this document! Even smallest 8 | contributions are important. 9 | 10 | If you're not sure how to contribute, please have a look to [our dedicated 11 | document](CONTRIBUTING.md). 12 | 13 | For the moment the list is quite short since Lessy opened its community quite 14 | recently. […] Well, yes, I'm almost alone to work on this project for the 15 | moment. One more good reason to join me :). 16 | 17 | Please keep the list sorted by name. 18 | 19 | --- 20 | 21 | **Marien Fressinaud** 22 | 23 | - [GitHub profile](https://github.com/marienfressinaud) 24 | - website: [marienfressinaud.fr](https://marienfressinaud.fr/) 25 | - email: [lessy@marienfressinaud.fr](mailto:lessy@marienfressinaud.fr) 26 | - Twitter: [@berumuron](https://twitter.com/berumuron) 27 | 28 | Note I'm available to have a drink while discussing of the project. I also can 29 | move to different cities than Grenoble (where I stay). It would be good 30 | occasions to visit! :) 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4-alpine 2 | MAINTAINER Marien Fressinaud 3 | 4 | EXPOSE 3000 5 | 6 | WORKDIR /app/ 7 | COPY Gemfile Gemfile.lock /app/ 8 | COPY client/package.json client/package-lock.json /app/client/ 9 | 10 | ENV RAILS_ENV production 11 | ENV RAILS_SERVE_STATIC_FILES true 12 | ENV RAILS_LOG_TO_STDOUT true 13 | 14 | RUN apk add --no-cache \ 15 | nodejs \ 16 | nodejs-npm \ 17 | postgresql-client \ 18 | tzdata \ 19 | && apk --update add --virtual build-dependencies \ 20 | build-base \ 21 | ruby-dev \ 22 | postgresql-dev \ 23 | libc-dev \ 24 | linux-headers \ 25 | cmake \ 26 | gmp-dev \ 27 | && gem install bundler \ 28 | && bundle install --without test development \ 29 | && apk del build-dependencies 30 | 31 | COPY . /app 32 | 33 | RUN cd /app/client \ 34 | && npm install \ 35 | && npm cache clean --force \ 36 | && npm run build \ 37 | && rm -rf /app/client/node_modules 38 | 39 | CMD ["bundle", "exec", "rails", "server", "-p", "3000"] 40 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4-alpine 2 | MAINTAINER Marien Fressinaud 3 | 4 | EXPOSE 3000 5000 5 | 6 | WORKDIR /app/ 7 | 8 | RUN apk add --no-cache \ 9 | nodejs \ 10 | nodejs-npm \ 11 | postgresql-client \ 12 | tzdata 13 | RUN apk --update add --virtual build-dependencies \ 14 | build-base \ 15 | ruby-dev \ 16 | postgresql-dev \ 17 | libc-dev \ 18 | linux-headers \ 19 | cmake \ 20 | gmp-dev 21 | 22 | RUN gem install bundler 23 | 24 | COPY Gemfile Gemfile.lock /app/ 25 | COPY client/package.json client/package-lock.json /app/client/ 26 | 27 | VOLUME /app 28 | 29 | CMD ["bundle", "exec", "foreman", "start"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marien Fressinaud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | run: ## Run development environment 4 | docker-compose -f docker-compose-dev.yml up 5 | 6 | build: ## Build Docker image 7 | docker-compose -f docker-compose-dev.yml build 8 | 9 | db-setup: ## Initialize development database 10 | docker-compose -f docker-compose-dev.yml run --rm lessy bundle exec rails db:setup 11 | 12 | install: ## Install dependencies 13 | docker-compose -f docker-compose-dev.yml run --rm --no-deps lessy bundle install 14 | docker-compose -f docker-compose-dev.yml run --rm --no-deps -w /app/client lessy npm install 15 | 16 | stop: ## Stop development environment 17 | docker-compose -f docker-compose-dev.yml stop 18 | 19 | clean: ## Clean development environment 20 | docker-compose -f docker-compose-dev.yml down 21 | 22 | test-back: ## Run tests (backend) 23 | docker-compose -f docker-compose-dev.yml run -e RAILS_ENV=test --rm lessy bundle exec rspec 24 | 25 | test-front: ## Run tests (frontend) 26 | docker-compose -f docker-compose-dev.yml run -e RAILS_ENV=test --rm -w /app/client lessy npm run test 27 | 28 | test: test-back test-front 29 | 30 | help: 31 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 32 | 33 | .PHONY: run build install update db-setup stop clean test test-back test-front help 34 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: cd client && npm run dev 2 | api: rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b 0.0.0.0 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | identified_by :current_user 4 | 5 | def connect 6 | self.current_user = find_verified_user 7 | end 8 | 9 | private 10 | 11 | def find_verified_user 12 | if current_user = User.find_by_authorization_token(cookies['Authorization']) 13 | current_user 14 | else 15 | reject_unauthorized_connection 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/channels/notifications_channel.rb: -------------------------------------------------------------------------------- 1 | class NotificationsChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_for current_user 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/admin/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # All Administrate controllers inherit from this `Admin::ApplicationController`, 4 | # making it the ideal place to put authentication logic or other 5 | # before_actions. 6 | # 7 | # If you want to add pagination or other controller-level concerns, 8 | # you're free to overwrite the RESTful controller actions. 9 | module Admin 10 | class ApplicationController < Administrate::ApplicationController 11 | before_action :authenticate_admin 12 | 13 | def authenticate_admin 14 | redirect_to :admin_login unless admin_signed_in? 15 | end 16 | 17 | private 18 | 19 | def admin_signed_in? 20 | logged_in? && current_user.admin? 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/admin/terms_of_services_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Admin 4 | class TermsOfServicesController < Admin::ApplicationController 5 | # To customize the behavior of this controller, 6 | # you can overwrite any of the RESTful actions. For example: 7 | # 8 | # def index 9 | # super 10 | # @resources = TermsOfService. 11 | # page(params[:page]). 12 | # per(10) 13 | # end 14 | 15 | # Define a custom finder by overriding the `find_resource` method: 16 | # def find_resource(param) 17 | # TermsOfService.find_by!(slug: param) 18 | # end 19 | 20 | # See https://administrate-prototype.herokuapp.com/customizing_controller_actions 21 | # for more information 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/admin/user_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Admin 4 | class UserSessionsController < ApplicationController 5 | skip_before_action :authenticate_admin, except: [:destroy] 6 | 7 | def new 8 | @user = User.new 9 | end 10 | 11 | def create 12 | @user = login(params[:username], params[:password]) 13 | if @user.try :admin? 14 | redirect_back_or_to(:admin_root, notice: 'Login successful') 15 | elsif @user 16 | logout 17 | flash.now[:alert] = 'You are not authorized to access administration, sorry!' 18 | render action: 'new' 19 | else 20 | flash.now[:alert] = 'Login failed' 21 | render action: 'new' 22 | end 23 | end 24 | 25 | def destroy 26 | logout 27 | redirect_to(:admin_root, notice: 'Logged out!') 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Admin 4 | class UsersController < Admin::ApplicationController 5 | # To customize the behavior of this controller, 6 | # you can overwrite any of the RESTful actions. For example: 7 | # 8 | # def index 9 | # super 10 | # @resources = User. 11 | # page(params[:page]). 12 | # per(10) 13 | # end 14 | 15 | # Define a custom finder by overriding the `find_resource` method: 16 | # def find_resource(param) 17 | # User.find_by!(slug: param) 18 | # end 19 | 20 | # See https://administrate-prototype.herokuapp.com/customizing_controller_actions 21 | # for more information 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/api/projects_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ProjectsController < ApiController 2 | 3 | def show 4 | @project = current_project 5 | end 6 | 7 | def update 8 | @project = current_project 9 | @project.update! update_project_params 10 | 11 | NotificationsChannel.broadcast_to( 12 | current_user, 13 | action: 'update#projects', 14 | id: @project.id, 15 | updatedAt: @project.updated_at.to_i, 16 | ) 17 | end 18 | 19 | def destroy 20 | current_project.destroy! 21 | end 22 | 23 | def update_state 24 | @project = current_project 25 | @project.update_with_transition! update_project_state_params 26 | 27 | NotificationsChannel.broadcast_to( 28 | current_user, 29 | action: 'update#projects', 30 | id: @project.id, 31 | updatedAt: @project.updated_at.to_i, 32 | ) 33 | end 34 | 35 | private 36 | 37 | def current_project 38 | @current_project ||= current_user.projects.find(params[:id]) 39 | end 40 | 41 | def update_project_params 42 | permitted_params = %i[name description] 43 | permitted_params << :due_at if current_project.started? 44 | params.require(:project).permit(*permitted_params) 45 | end 46 | 47 | def update_project_state_params 48 | state = params[:project][:state] 49 | if state == 'started' 50 | fetch_resource_params(:project, %i[state due_at]) 51 | elsif state == 'finished' 52 | fetch_resource_params(:project, %i[state finished_at]) 53 | else 54 | fetch_resource_params(:project, %i[state]) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/controllers/api/tasks_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::TasksController < ApiController 2 | 3 | def show 4 | @task = current_task 5 | end 6 | 7 | def update 8 | @task = current_task 9 | @task.assign_attributes update_task_params 10 | @task.sync_state_with_project 11 | @task.save! 12 | 13 | NotificationsChannel.broadcast_to( 14 | current_user, 15 | action: 'update#tasks', 16 | id: @task.id, 17 | updatedAt: @task.updated_at.to_i, 18 | ) 19 | end 20 | 21 | def update_state 22 | @task = current_task 23 | @task.update_with_transition! update_task_state_params 24 | 25 | NotificationsChannel.broadcast_to( 26 | current_user, 27 | action: 'update#tasks', 28 | id: @task.id, 29 | updatedAt: @task.updated_at.to_i, 30 | ) 31 | end 32 | 33 | def update_order 34 | task = current_task 35 | order = update_task_order_params[:order] 36 | @impacted_tasks = [] 37 | @impacted_tasks = task.order_incremental!(order) if order < task.order 38 | @impacted_tasks = task.order_decremental!(order) if order > task.order 39 | end 40 | 41 | private 42 | 43 | def current_task 44 | @current_task ||= current_user.tasks.find(params[:id]) 45 | end 46 | 47 | def update_task_params 48 | fetch_resource_params(:task, [], [:label, :project_id]) 49 | end 50 | 51 | def update_task_order_params 52 | fetch_resource_params(:task, [:order]) 53 | end 54 | 55 | def update_task_state_params 56 | fetch_resource_params(:task, [:state]) 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /app/controllers/api/terms_of_services_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class TermsOfServicesController < ApiController 5 | skip_before_action :require_login, only: [:current] 6 | skip_before_action :require_tos_accepted, only: [:current] 7 | 8 | def current 9 | @terms_of_service = TermsOfService.current 10 | head :no_content if @terms_of_service.nil? 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/api/users/activation_emails_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::Users::ActivationEmailsController < ApiController 4 | skip_before_action :require_login, only: [:create] 5 | skip_before_action :require_tos_accepted, only: [:create] 6 | 7 | def create 8 | user = User.find_by!(find_user_params) 9 | UserMailer.activation_needed_email(user).deliver_later if user.inactive? 10 | head :no_content 11 | end 12 | 13 | private 14 | 15 | def find_user_params 16 | fetch_resource_params(:user, [:email]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/api/users/activations_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Users::ActivationsController < ApiController 2 | skip_before_action :require_login, only: [:create] 3 | skip_before_action :require_tos_accepted, only: [:create] 4 | 5 | def create 6 | @user = User.find_by_sorcery_token!(params[:token], type: :activation) 7 | @user.update! activate_user_params 8 | @user.activate! 9 | @token = @user.token(expiration: 1.month.from_now) 10 | end 11 | 12 | private 13 | 14 | def activate_user_params 15 | fetch_resource_params(:user, [:username, :password]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/api/users/authorizations_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Users::AuthorizationsController < ApiController 2 | skip_before_action :require_login, only: [:create] 3 | skip_before_action :require_tos_accepted, only: [:create] 4 | 5 | def create 6 | @user = User.authenticate(params[:username], params[:password]) 7 | unless @user 8 | render_error ApiErrors::LoginFailed.new, :unauthorized 9 | return 10 | end 11 | sudo_mode = params[:sudo] ? true : false 12 | token_duration_validity = sudo_mode ? 15.minutes : 1.month 13 | @token = @user.token( 14 | expiration: token_duration_validity.from_now, 15 | sudo: sudo_mode, 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/api/users/features_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::Users::FeaturesController < ApiController 4 | skip_before_action :require_tos_accepted, only: [:index] 5 | 6 | def index 7 | @features = current_user.features_enabled 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/users/password_resets_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::Users::PasswordResetsController < ApiController 4 | skip_before_action :require_login, only: [:create] 5 | skip_before_action :require_tos_accepted, only: [:create] 6 | 7 | before_action :set_user 8 | before_action do 9 | require_active_user(@user) 10 | end 11 | 12 | def create 13 | @user.deliver_reset_password_instructions! 14 | head :no_content 15 | end 16 | 17 | private 18 | 19 | def set_user 20 | find_user_params = fetch_resource_params(:user, [:email]) 21 | @user = User.find_by!(find_user_params) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/api/users/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::Users::PasswordsController < ApiController 4 | skip_before_action :require_login, if: -> { params[:token] } 5 | skip_before_action :require_tos_accepted, if: -> { params[:token] } 6 | before_action :require_sudo, if: -> { !params[:token] } 7 | 8 | before_action :set_user, only: [:create] 9 | before_action do 10 | require_active_user(@user) 11 | end 12 | 13 | def create 14 | @user.change_password! password_param 15 | @token = @user.token(expiration: 1.month.from_now) if params[:token] 16 | end 17 | 18 | private 19 | 20 | def set_user 21 | @user = if params[:token] 22 | User.find_by_sorcery_token!(params[:token], type: :reset_password) 23 | else 24 | current_user 25 | end 26 | end 27 | 28 | def password_param 29 | fetch_resource_params(:user, [:password])[:password] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/api/users/projects_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Users::ProjectsController < ApiController 2 | skip_before_action :require_tos_accepted, only: [:index] 3 | 4 | def index 5 | @projects = current_user 6 | .projects 7 | .order(:id) 8 | .page(params[:page]) 9 | end 10 | 11 | def create 12 | @project = Project.create!(create_project_params) 13 | 14 | NotificationsChannel.broadcast_to( 15 | current_user, 16 | action: 'create#projects', 17 | id: @project.id, 18 | ) 19 | 20 | render status: :created 21 | end 22 | 23 | private 24 | 25 | def create_project_params 26 | fetch_resource_params(:project, [:name]).merge(user: current_user) 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/api/users/tasks_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Users::TasksController < ApiController 2 | skip_before_action :require_tos_accepted, only: [:index] 3 | 4 | def index 5 | @tasks = current_user 6 | .tasks 7 | .not_abandoned 8 | .not_finished_before(2.weeks.ago) 9 | .order(:id) 10 | .page(params[:page]) 11 | end 12 | 13 | def create 14 | @task = current_user.tasks.new(create_task_params) 15 | @task.sync_state_with_project 16 | lock_name = "sync_order_for_user_#{current_user.id}" 17 | Task.with_advisory_lock(lock_name) do 18 | @task.sync_order 19 | @task.save! 20 | end 21 | 22 | NotificationsChannel.broadcast_to( 23 | current_user, 24 | action: 'create#tasks', 25 | id: @task.id, 26 | ) 27 | 28 | render status: :created 29 | end 30 | 31 | private 32 | 33 | def create_task_params 34 | parameters = fetch_resource_params(:task, [:label], %i[planned_at finished_at project_id]) 35 | 36 | if parameters.key?(:planned_at) 37 | parameters[:state] = 'planned' 38 | parameters[:started_at] = Time.current 39 | end 40 | 41 | if parameters.key?(:finished_at) 42 | parameters[:state] = 'finished' 43 | parameters[:planned_at] = Time.current unless parameters.key?(:planned_at) 44 | parameters[:started_at] = Time.current 45 | end 46 | 47 | parameters[:state] = 'newed' unless parameters.key?(:state) 48 | 49 | parameters 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApiController 2 | skip_before_action :require_login, only: [:create] 3 | skip_before_action :require_tos_accepted, only: %i[create show destroy accept_tos] 4 | 5 | before_action :require_registration_enabled, only: :create 6 | before_action :require_active_user, only: :update 7 | before_action only: :destroy do 8 | require_sudo if current_user.active? 9 | end 10 | 11 | def create 12 | @user = User.create!(create_user_params) 13 | @token = @user.token(expiration: 1.day.from_now) 14 | render status: :created 15 | end 16 | 17 | def show 18 | @user = current_user 19 | end 20 | 21 | def update 22 | @user = current_user 23 | @user.update! update_user_params 24 | end 25 | 26 | def destroy 27 | current_user.destroy! 28 | end 29 | 30 | def accept_tos 31 | @user = current_user 32 | @user.update! terms_of_service: TermsOfService.current 33 | end 34 | 35 | private 36 | 37 | def require_registration_enabled 38 | registration_disabled = !Flipper.enabled?(:feature_registration) 39 | render_error ApiErrors::RegistrationDisabled.new, :forbidden if registration_disabled 40 | end 41 | 42 | def create_user_params 43 | fetch_resource_params(:user, [:email]) 44 | .merge(terms_of_service: TermsOfService.current) 45 | end 46 | 47 | def update_user_params 48 | fetch_resource_params(:user, [], %i[email username time_zone]) 49 | .delete_if { |k, v| v.nil? } 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/api/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::WelcomeController < ApiController 2 | 3 | skip_before_action :require_login 4 | skip_before_action :require_tos_accepted 5 | 6 | def index 7 | end 8 | 9 | def not_found 10 | render_error ApiErrors::MissingEndpoint.new, :not_found 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | 3 | def client 4 | render file: 'public/index.html' 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/dashboards/terms_of_service_dashboard.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'administrate/base_dashboard' 4 | 5 | class TermsOfServiceDashboard < Administrate::BaseDashboard 6 | # ATTRIBUTE_TYPES 7 | # a hash that describes the type of each of the model's fields. 8 | # 9 | # Each different type represents an Administrate::Field object, 10 | # which determines how the attribute is displayed 11 | # on pages throughout the dashboard. 12 | ATTRIBUTE_TYPES = { 13 | id: Field::Number, 14 | content: Field::Text, 15 | version: Field::String, 16 | effective_at: Field::DateTime, 17 | created_at: Field::DateTime, 18 | }.freeze 19 | 20 | # COLLECTION_ATTRIBUTES 21 | # an array of attributes that will be displayed on the model's index page. 22 | # 23 | # By default, it's limited to four items to reduce clutter on index pages. 24 | # Feel free to add, remove, or rearrange items. 25 | COLLECTION_ATTRIBUTES = %i[ 26 | id 27 | version 28 | effective_at 29 | ].freeze 30 | 31 | # SHOW_PAGE_ATTRIBUTES 32 | # an array of attributes that will be displayed on the model's show page. 33 | SHOW_PAGE_ATTRIBUTES = %i[ 34 | content 35 | version 36 | effective_at 37 | created_at 38 | ].freeze 39 | 40 | # FORM_ATTRIBUTES 41 | # an array of attributes that will be displayed 42 | # on the model's form (`new` and `edit`) pages. 43 | FORM_ATTRIBUTES = %i[ 44 | content 45 | version 46 | effective_at 47 | ].freeze 48 | 49 | # Overwrite this method to customize how users are displayed 50 | # across all pages of the admin dashboard. 51 | def display_resource(terms_of_service) 52 | "ToS #{terms_of_service.version}" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/lib/core_extensions/action_controller/resource_parameter_missing.rb: -------------------------------------------------------------------------------- 1 | module ActionController 2 | class ResourceParameterMissing < ParameterMissing 3 | attr_reader :resource 4 | 5 | def initialize(resource, param) 6 | @resource = resource 7 | super(param) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/lib/flipper_migration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FlipperMigration 4 | class FlipperFeature < ApplicationRecord 5 | has_many :flipper_gates, dependent: :destroy, 6 | foreign_key: :feature_key, 7 | primary_key: :key 8 | end 9 | 10 | class FlipperGate < ApplicationRecord 11 | end 12 | 13 | def create_flag(flag_key, enabled: false) 14 | if enabled 15 | Flipper.enable flag_key 16 | else 17 | Flipper.disable flag_key 18 | end 19 | end 20 | 21 | def destroy_flag(flag_key) 22 | FlipperFeature.find_by!(key: flag_key)&.destroy 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/lib/json_web_token.rb: -------------------------------------------------------------------------------- 1 | class JsonWebToken 2 | def self.encode(data, expiration) 3 | payload = { 4 | data: data, 5 | exp: expiration.to_i, 6 | } 7 | JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256') 8 | end 9 | 10 | def self.decode(token) 11 | body = JWT.decode(token, Rails.application.secrets.secret_key_base, 'HS256')[0] 12 | HashWithIndifferentAccess.new body 13 | rescue JWT::DecodeError => e 14 | Rails.logger.warn "JsonWebToken.decode: #{ e.message }" 15 | nil 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | 3 | default from: 'noreply@lessy.io' 4 | layout 'mailer' 5 | 6 | protected 7 | 8 | def mail(options={}) 9 | options[:subject] = "[Lessy] #{ options[:subject] }" if options.has_key? :subject 10 | super options 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | class UserMailer < ApplicationMailer 2 | def activation_needed_email(user) 3 | @user = user 4 | @activation_url = "#{ root_url }users/#{ user.activation_token }/activate" 5 | mail to: user.email, 6 | subject: 'Welcome to Lessy!' 7 | end 8 | 9 | def activation_success_email(user) 10 | @user = user 11 | mail to: user.email, 12 | subject: 'Your account is now activated' 13 | end 14 | 15 | def reset_password_email(user) 16 | @user = user 17 | @reset_url = "#{root_url}password/#{user.reset_password_token}/new" 18 | mail to: user.email, 19 | subject: 'Lessy password reset' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/flipper_feature.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FlipperFeature < ApplicationRecord 4 | has_many :flipper_gates, dependent: :destroy, 5 | foreign_key: :feature_key, 6 | primary_key: :key, 7 | inverse_of: :flipper_feature 8 | end 9 | -------------------------------------------------------------------------------- /app/models/flipper_gate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FlipperGate < ApplicationRecord 4 | belongs_to :flipper_feature, foreign_key: :feature_key, 5 | primary_key: :key, 6 | inverse_of: :flipper_gate 7 | end 8 | -------------------------------------------------------------------------------- /app/models/project.rb: -------------------------------------------------------------------------------- 1 | class Project < ApplicationRecord 2 | include ProjectLifecycle 3 | 4 | belongs_to :user 5 | has_many :tasks, dependent: :nullify 6 | 7 | validates :user, :name, :slug, presence: true 8 | validates :slug, uniqueness: { scope: :user, message: 'must be unique per user' }, 9 | format: { with: /\A[\w\-]{1,}\z/, message: 'must contain letters, numbers, underscores (_) and hiphens (-) only' } 10 | validates :name, length: { maximum: 100 } 11 | 12 | paginates_per 25 13 | 14 | before_validation do 15 | self.slug = self.name.parameterize 16 | end 17 | 18 | before_destroy :abandon_tasks, prepend: true 19 | 20 | private 21 | 22 | def abandon_tasks 23 | # rubocop:disable Rails/SkipsModelValidations 24 | tasks.update_all state: :abandoned, abandoned_at: Time.zone.now 25 | # rubocop:enable Rails/SkipsModelValidations 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/models/terms_of_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TermsOfService < ApplicationRecord 4 | has_many :users, dependent: :nullify 5 | 6 | validates :content, :version, :effective_at, presence: true 7 | validates :version, uniqueness: { case_sensitive: false } 8 | 9 | def self.current 10 | TermsOfService 11 | .where('effective_at <= ?', Time.zone.now) 12 | .order(:effective_at) 13 | .last 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/admin/application/_navigation.html.erb: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /app/views/admin/user_sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:title) do %> 2 | Admin login 3 | <% end %> 4 | 5 | 10 | 11 |
12 | <%= form_tag admin_user_sessions_path, method: :post, class: 'form' do %> 13 |
14 |
15 | <%= label_tag :username %> 16 |
17 |
18 | <%= text_field_tag :username %> 19 |
20 |
21 | 22 |
23 |
24 | <%= label_tag :password %> 25 |
26 |
27 | <%= password_field_tag :password %> 28 |
29 |
30 | 31 |
32 | <%= submit_tag 'Login' %> 33 | <%= link_to 'Back to Lessy', root_path %> 34 |
35 | <% end %> 36 |
37 | -------------------------------------------------------------------------------- /app/views/api/errors.jbuilder: -------------------------------------------------------------------------------- 1 | json.errors @errors do |error| 2 | json.extract! error, :status, :code, :title, :detail 3 | if error.source_pointer.present? 4 | json.source do 5 | json.pointer error.source_pointer 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/api/projects/_project.jbuilder: -------------------------------------------------------------------------------- 1 | json.type 'project' 2 | json.id project.id 3 | json.attributes do 4 | json.extract! project, :name, :slug, :description, :state, 5 | :started_at, :due_at, :paused_at, :finished_at, 6 | :created_at, :updated_at 7 | end 8 | json.relationships do 9 | json.user do 10 | json.data do 11 | json.type 'user' 12 | json.id project.user_id 13 | end 14 | end 15 | json.tasks do 16 | json.data project.tasks.not_abandoned do |task| 17 | json.type 'task' 18 | json.id task.id 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/api/projects/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @project, partial: 'api/projects/project', as: :project 2 | -------------------------------------------------------------------------------- /app/views/api/projects/update.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @project, partial: 'api/projects/project', as: :project 2 | -------------------------------------------------------------------------------- /app/views/api/projects/update_state.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @project, partial: 'api/projects/project', as: :project 2 | -------------------------------------------------------------------------------- /app/views/api/tasks/_task.jbuilder: -------------------------------------------------------------------------------- 1 | json.type 'task' 2 | json.id task.id 3 | json.attributes do 4 | json.extract! task, :label, :order, :planned_count, :state, 5 | :started_at, :planned_at, :finished_at, :abandoned_at, 6 | :created_at, :updated_at 7 | end 8 | json.relationships do 9 | json.user do 10 | json.data do 11 | json.type 'user' 12 | json.id task.user_id 13 | end 14 | end 15 | json.project do 16 | json.data do 17 | if task.project.nil? 18 | json.nil! 19 | else 20 | json.type 'project' 21 | json.id task.project_id 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/views/api/tasks/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @task, partial: 'api/tasks/task', as: :task 2 | -------------------------------------------------------------------------------- /app/views/api/tasks/update.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @task, partial: 'api/tasks/task', as: :task 2 | -------------------------------------------------------------------------------- /app/views/api/tasks/update_order.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @impacted_tasks do |task| 2 | json.type 'task' 3 | json.id task.id 4 | json.attributes do 5 | json.order task.reload.order 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/tasks/update_state.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @task, partial: 'api/tasks/task', as: :task 2 | -------------------------------------------------------------------------------- /app/views/api/terms_of_services/current.jbuilder: -------------------------------------------------------------------------------- 1 | json.data do 2 | json.id @terms_of_service.id 3 | json.type 'terms_of_service' 4 | json.attributes do 5 | json.extract! @terms_of_service, :content, :version 6 | json.effective_at @terms_of_service.effective_at.to_i 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/api/users/_user.jbuilder: -------------------------------------------------------------------------------- 1 | json.id user.id 2 | json.type 'user' 3 | json.attributes do 4 | json.extract! user, :username, :email, :admin, :time_zone 5 | json.has_accepted_tos user.accepted_tos? 6 | end 7 | -------------------------------------------------------------------------------- /app/views/api/users/accept_tos.jbuilder: -------------------------------------------------------------------------------- 1 | json.data do 2 | json.id @user.id 3 | json.type 'user' 4 | json.attributes do 5 | json.has_accepted_tos @user.accepted_tos? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/users/activations/create.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @user, partial: 'api/users/user', as: :user 2 | json.meta do 3 | json.token @token 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/users/authorizations/create.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @user, partial: 'api/users/user', as: :user 2 | json.meta do 3 | json.token @token 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/users/create.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @user, partial: 'api/users/user', as: :user 2 | json.meta do 3 | json.token @token 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/users/features/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @features do |feature| 2 | json.type 'feature' 3 | json.id feature.name 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/users/passwords/create.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @user, partial: 'api/users/user', as: :user 2 | if @token 3 | json.meta do 4 | json.token @token 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/api/users/projects/create.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @project, partial: 'api/projects/project', as: :project 2 | -------------------------------------------------------------------------------- /app/views/api/users/projects/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @projects, partial: 'api/projects/project', as: :project 2 | json.links do 3 | json.first me_projects_api_users_path(page: 1) 4 | json.last me_projects_api_users_path(page: @projects.total_pages) 5 | if !@projects.first_page? && @projects.total_pages != 0 6 | json.prev me_projects_api_users_path(page: @projects.prev_page) 7 | end 8 | if !@projects.last_page? && @projects.total_pages != 0 9 | json.next me_projects_api_users_path(page: @projects.next_page) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/api/users/show.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @user, partial: 'api/users/user', as: :user 2 | -------------------------------------------------------------------------------- /app/views/api/users/tasks/create.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @task, partial: 'api/tasks/task', as: :task 2 | -------------------------------------------------------------------------------- /app/views/api/users/tasks/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.data @tasks, partial: 'api/tasks/task', as: :task 2 | json.links do 3 | json.first me_tasks_api_users_path(page: 1) 4 | json.last me_tasks_api_users_path(page: @tasks.total_pages) 5 | if !@tasks.first_page? && @tasks.total_pages != 0 6 | json.prev me_tasks_api_users_path(page: @tasks.prev_page) 7 | end 8 | if !@tasks.last_page? && @tasks.total_pages != 0 9 | json.next me_tasks_api_users_path(page: @tasks.next_page) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/api/users/update.jbuilder: -------------------------------------------------------------------------------- 1 | json.data do 2 | json.id @user.id 3 | json.type 'user' 4 | json.attributes do 5 | json.extract! @user, :username, :email, :time_zone 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/welcome/index.jbuilder: -------------------------------------------------------------------------------- 1 | json.ignore_nil! 2 | json.registration_disabled !Flipper.enabled?(:feature_registration) 3 | json.tos_version TermsOfService.current&.version 4 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | 3 | Have a wonderful day! 4 | -------------------------------------------------------------------------------- /app/views/user_mailer/activation_needed_email.text.erb: -------------------------------------------------------------------------------- 1 | Welcome <%= @user.email %>, 2 | 3 | You've just registered on <%= root_url %> but you still need to activate your account by setting a password. Just follow the link: 4 | 5 | <%= @activation_url %> 6 | -------------------------------------------------------------------------------- /app/views/user_mailer/activation_success_email.text.erb: -------------------------------------------------------------------------------- 1 | Well done <%= @user.username %>, 2 | 3 | You've enabled your account on <%= root_url %> with success, let's have fun now! 4 | -------------------------------------------------------------------------------- /app/views/user_mailer/reset_password_email.text.erb: -------------------------------------------------------------------------------- 1 | Hello <%= @user.username %>, 2 | 3 | Somebody asked to reset your account on <%= root_url %> 4 | 5 | If it was not you, you can safely ignore this email. 6 | 7 | Click the following link to choose a new password: <%= @reset_url %> 8 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | if spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 13 | gem 'spring', spring.version 14 | require 'spring/binstub' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2", 5 | ["env", { "modules": false }] 6 | ], 7 | "plugins": ["transform-runtime"], 8 | "comments": false, 9 | "env": { 10 | "test": { 11 | "presets": [ 12 | ["env", { "targets": { "node": "current" }}] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /client/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true 4 | }, 5 | globals: { 6 | "require": "readonly", 7 | }, 8 | root: true, 9 | parser: 'babel-eslint', 10 | parserOptions: { 11 | ecmaVersion: 6, 12 | sourceType: 'module' 13 | }, 14 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 15 | extends: 'standard', 16 | // required to lint *.vue files 17 | plugins: [ 18 | 'html' 19 | ], 20 | // add your custom rules here 21 | 'rules': { 22 | // allow paren-less arrow functions 23 | 'arrow-parens': 0, 24 | // allow async-await 25 | 'generator-star-spacing': 0, 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 28 | // allow trailing commas 29 | 'comma-dangle': 0, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | test/unit/coverage 6 | -------------------------------------------------------------------------------- /client/.scss.yml: -------------------------------------------------------------------------------- 1 | scss_files: 'src/**/*.scss' 2 | 3 | linters: 4 | LeadingZero: 5 | style: exclude_zero 6 | 7 | PropertySortOrder: 8 | order: 9 | - position 10 | - top 11 | - right 12 | - bottom 13 | - left 14 | - z-index 15 | - 16 | - display 17 | - float 18 | - width 19 | - height 20 | - margin 21 | - margin-top 22 | - margin-bottom 23 | - margin-left 24 | - margin-right 25 | - padding 26 | - padding-top 27 | - padding-bottom 28 | - padding-left 29 | - padding-right 30 | - 31 | - font 32 | - line-height 33 | - color 34 | - text-align 35 | - 36 | - background 37 | - border 38 | - border-radius 39 | - box-shadow 40 | - 41 | - opacity 42 | separate_groups: true 43 | -------------------------------------------------------------------------------- /client/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // if you are using ts-loader, setting this to true will make typescript errors show up during build 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /client/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const config = require('../config') 3 | 4 | module.exports = { 5 | cacheBusting: config.dev.cacheBusting, 6 | transformAssetUrls: { 7 | video: ['src', 'poster'], 8 | source: 'src', 9 | img: 'src', 10 | image: 'xlink:href' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // This is the webpack config used for unit tests. 3 | 4 | const utils = require('./utils') 5 | const webpack = require('webpack') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | 9 | const webpackConfig = merge(baseWebpackConfig, { 10 | // use inline sourcemap for karma-sourcemap-loader 11 | module: { 12 | rules: utils.styleLoaders() 13 | }, 14 | devtool: '#inline-source-map', 15 | resolveLoader: { 16 | alias: { 17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 19 | 'scss-loader': 'sass-loader' 20 | } 21 | }, 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': require('../config/test.env') 25 | }) 26 | ] 27 | }) 28 | 29 | // no need for app entry during tests 30 | delete webpackConfig.entry 31 | 32 | module.exports = webpackConfig 33 | -------------------------------------------------------------------------------- /client/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /client/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /client/config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Lessy 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/spec/components/Ly/LyButton.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import LyCard from '@/components/Ly/LyCard' 3 | 4 | describe('LyCard', () => { 5 | describe('name class', () => { 6 | test('is set to the given prop', () => { 7 | const wrapper = mount(LyCard) 8 | 9 | wrapper.setProps({ name: 'hello' }) 10 | 11 | expect(wrapper.classes()).toContain('ly-card-hello') 12 | }) 13 | }) 14 | 15 | describe('image prop', () => { 16 | test('a background image is set', () => { 17 | const wrapper = mount(LyCard) 18 | 19 | wrapper.setProps({ image: '/static/back.jpg' }) 20 | 21 | const renderedCard = wrapper.html() 22 | expect(renderedCard).toMatch(/background-image: url\(\/static\/back.jpg\)/) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /client/spec/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest' 16 | }, 17 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 18 | setupFiles: ['/spec/setup'], 19 | coverageDirectory: '/spec/coverage', 20 | collectCoverageFrom: [ 21 | 'src/**/*.{js,vue}', 22 | '!src/main.js', 23 | '!src/router.js', 24 | '!**/node_modules/**' 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /client/spec/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /client/src/api/features.js: -------------------------------------------------------------------------------- 1 | import { get } from './http' 2 | 3 | export default { 4 | list () { 5 | return get('/api/users/me/features') 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /client/src/api/projects.js: -------------------------------------------------------------------------------- 1 | import { get, post, patch, put, destroy } from './http' 2 | 3 | export default { 4 | list () { 5 | return get('/api/users/me/projects?page=1') 6 | }, 7 | 8 | get (id) { 9 | return get(`/api/projects/${id}`) 10 | }, 11 | 12 | create (name) { 13 | return post('/api/users/me/projects', { name }) 14 | }, 15 | 16 | update (project, payload) { 17 | return patch(`/api/projects/${project.id}`, payload) 18 | }, 19 | 20 | start (project, dueAt) { 21 | return put(`/api/projects/${project.id}/state`, { 22 | project: { 23 | state: 'started', 24 | due_at: dueAt, 25 | }, 26 | }) 27 | }, 28 | 29 | finish (project, finishedAt) { 30 | return put(`/api/projects/${project.id}/state`, { 31 | project: { 32 | state: 'finished', 33 | finished_at: finishedAt, 34 | }, 35 | }) 36 | }, 37 | 38 | pause (project) { 39 | return put(`/api/projects/${project.id}/state`, { 40 | project: { 41 | state: 'paused', 42 | }, 43 | }) 44 | }, 45 | 46 | delete (project) { 47 | return destroy(`/api/projects/${project.id}`) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/src/api/root.js: -------------------------------------------------------------------------------- 1 | import { get } from './http' 2 | 3 | export default { 4 | listInfo () { 5 | return get('/api') 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /client/src/api/tasks.js: -------------------------------------------------------------------------------- 1 | import { get, post, patch, put } from './http' 2 | 3 | export default { 4 | list () { 5 | return get('/api/users/me/tasks?page=1') 6 | }, 7 | 8 | get (id) { 9 | return get(`/api/tasks/${id}`) 10 | }, 11 | 12 | create (payload) { 13 | return post('/api/users/me/tasks', payload) 14 | }, 15 | 16 | update (task, payload) { 17 | return patch(`/api/tasks/${task.id}`, payload) 18 | }, 19 | 20 | finish (task) { 21 | return put(`/api/tasks/${task.id}/state`, { 22 | state: 'finished', 23 | }) 24 | }, 25 | 26 | start (task) { 27 | return put(`/api/tasks/${task.id}/state`, { 28 | state: 'planned', 29 | }) 30 | }, 31 | 32 | unplan (task) { 33 | return put(`/api/tasks/${task.id}/state`, { 34 | state: 'started', 35 | }) 36 | }, 37 | 38 | abandon (task) { 39 | return put(`/api/tasks/${task.id}/state`, { 40 | state: 'abandoned', 41 | }) 42 | }, 43 | 44 | updateOrder (task, order) { 45 | const payload = { 46 | task: { 47 | order, 48 | }, 49 | } 50 | return put(`/api/tasks/${task.id}/order`, payload) 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /client/src/api/terms_of_services.js: -------------------------------------------------------------------------------- 1 | import { get } from './http' 2 | 3 | export default { 4 | getCurrent () { 5 | return get('/api/terms_of_services/current', { authorization: 'none' }) 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /client/src/auth.js: -------------------------------------------------------------------------------- 1 | export default { 2 | login (token) { 3 | window.localStorage.setItem('authentication_token', token) 4 | }, 5 | 6 | isLoggedIn () { 7 | return this.getToken('normal') != null 8 | }, 9 | 10 | sudo (token) { 11 | window.localStorage.setItem('sudo_token', token) 12 | }, 13 | 14 | isSudo () { 15 | return this.getToken('sudo') != null 16 | }, 17 | 18 | logout () { 19 | window.localStorage.removeItem('authentication_token') 20 | window.localStorage.removeItem('sudo_token') 21 | }, 22 | 23 | getToken (mode = 'normal') { 24 | const itemName = mode === 'normal' ? 'authentication_token' : 'sudo_token' 25 | return window.localStorage.getItem(itemName) 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/App/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /client/src/components/App/AppLogo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 66 | -------------------------------------------------------------------------------- /client/src/components/App/AppPage.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyCardDeck.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 42 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyColumns/LyColumn.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyColumns/LyColumns.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 45 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyColumns/index.js: -------------------------------------------------------------------------------- 1 | import LyColumns from './LyColumns' 2 | import LyColumn from './LyColumn' 3 | 4 | export { 5 | LyColumns, 6 | LyColumn, 7 | } 8 | export default LyColumns 9 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyForm/LyForm.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | 51 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyForm/LyFormGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 44 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyForm/LyFormTextarea.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 55 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyForm/index.js: -------------------------------------------------------------------------------- 1 | import LyForm from './LyForm' 2 | import LyFormGroup from './LyFormGroup' 3 | import LyFormInput from './LyFormInput' 4 | import LyFormSelect from './LyFormSelect' 5 | import LyFormTextarea from './LyFormTextarea' 6 | 7 | export { 8 | LyForm, 9 | LyFormGroup, 10 | LyFormInput, 11 | LyFormSelect, 12 | LyFormTextarea, 13 | } 14 | export default LyForm 15 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyIcon.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyList/LyListGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 46 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyList/LyListItemAdapt.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyList/index.js: -------------------------------------------------------------------------------- 1 | import LyList from './LyList' 2 | import LyListGroup from './LyListGroup' 3 | import LyListItem from './LyListItem' 4 | import LyListItemAdapt from './LyListItemAdapt' 5 | 6 | export { 7 | LyList, 8 | LyListGroup, 9 | LyListItem, 10 | LyListItemAdapt, 11 | } 12 | export default LyList 13 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyPopover/LyPopoverItem.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 40 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyPopover/LyPopoverSeparator.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyPopover/index.js: -------------------------------------------------------------------------------- 1 | import LyPopover from './LyPopover' 2 | import LyPopoverItem from './LyPopoverItem' 3 | import LyPopoverSeparator from './LyPopoverSeparator' 4 | 5 | export { 6 | LyPopover, 7 | LyPopoverItem, 8 | LyPopoverSeparator, 9 | } 10 | export default LyPopover 11 | -------------------------------------------------------------------------------- /client/src/components/Ly/LySection.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 68 | -------------------------------------------------------------------------------- /client/src/components/Ly/LyTextContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 33 | -------------------------------------------------------------------------------- /client/src/components/design/DesignGridPage.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /client/src/components/design/DesignVisualsPage.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /client/src/components/design/DesignWordingPage.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /client/src/components/general/LoadingPage.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 43 | 44 | 65 | -------------------------------------------------------------------------------- /client/src/components/general/NotFoundPage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | -------------------------------------------------------------------------------- /client/src/components/general/TermsOfServiceModal.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /client/src/components/layouts/LayoutDesign.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /client/src/components/layouts/LayoutEmpty.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /client/src/components/layouts/LayoutProfile.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 46 | -------------------------------------------------------------------------------- /client/src/components/mixins/ErrorsHandler.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data () { 3 | return { 4 | errors: {}, 5 | } 6 | }, 7 | 8 | methods: { 9 | setFailureErrors (failure) { 10 | const { data: { errors } } = failure 11 | let newErrors = {} 12 | 13 | errors.forEach(error => { 14 | const pointer = error.source == null ? '/_' : error.source.pointer 15 | if (newErrors[pointer] == null) { 16 | newErrors[pointer] = [] 17 | } 18 | newErrors[pointer] = [ 19 | ...newErrors[pointer], 20 | error.code, 21 | ] 22 | }) 23 | 24 | this.errors = newErrors 25 | }, 26 | 27 | getErrors (pointer = '/_') { 28 | const i18nPointer = pointer.replace(/\//g, '.') 29 | const i18nBase = `errors${i18nPointer}` 30 | return this.errors[pointer] && this.errors[pointer].map((errorCode) => 31 | this.$t(i18nBase + '.' + errorCode) 32 | ).join(' ') 33 | }, 34 | 35 | cleanErrors () { 36 | this.errors = {} 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/src/components/mixins/ResourcesLoader.js: -------------------------------------------------------------------------------- 1 | import auth from '@/auth' 2 | 3 | export default { 4 | computed: { 5 | resourcesReady () { 6 | return this.$store.getters['global/resourcesReady'] 7 | }, 8 | }, 9 | 10 | mounted () { 11 | this.$store 12 | .dispatch('users/getCurrent') 13 | .then(() => { 14 | return Promise.all([ 15 | this.$store.dispatch('features/list'), 16 | this.$store.dispatch('tasks/list'), 17 | this.$store.dispatch('projects/list'), 18 | ]) 19 | }) 20 | .then(() => { 21 | this.$store.commit('global/setResourcesReady', true) 22 | }) 23 | .catch((e) => { 24 | const isUnauthorized = e.data.errors.some(error => error.code === 'unauthorized') 25 | if (isUnauthorized) { 26 | // having troubles to fetch current user? It probably means token 27 | // expired or user does not exist. Logout and return to home page. 28 | auth.logout() 29 | window.location = '/' 30 | } 31 | }) 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /client/src/components/profile/ProfileDeleteAccount.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 67 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectCard.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 64 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectCardDeck.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 46 | 47 | 52 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 52 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectDeleteModal.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 34 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectEditDueDateForm.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 54 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectEditDueDateModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectEditPage.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectFinishForm.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectFinishModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectItemFinished.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectStartForm.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 56 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectStartModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectsHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /client/src/components/projects/ProjectsStartNewModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | -------------------------------------------------------------------------------- /client/src/components/tasks/TaskAttachProjectModal.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /client/src/components/tasks/TaskConfirmAbandonModal.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /client/src/components/tasks/TaskIndicators.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /client/src/components/tasks/TaskList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | -------------------------------------------------------------------------------- /client/src/components/tasks/TaskSelectList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /client/src/components/tasks/TaskSelectableItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | -------------------------------------------------------------------------------- /client/src/components/tasks/TaskTransformInProjectModal.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 42 | -------------------------------------------------------------------------------- /client/src/components/tasks/TasksPage.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 42 | -------------------------------------------------------------------------------- /client/src/components/tasks/TasksPlanModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 63 | -------------------------------------------------------------------------------- /client/src/components/users/UserActivatePage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 33 | -------------------------------------------------------------------------------- /client/src/components/users/UserLoginPage.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /client/src/components/users/UserPasswordNewForm.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 51 | -------------------------------------------------------------------------------- /client/src/components/users/UserPasswordNewPage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 33 | -------------------------------------------------------------------------------- /client/src/components/users/UserPasswordResetForm.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 53 | -------------------------------------------------------------------------------- /client/src/components/users/UserPopover.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 57 | 58 | 63 | -------------------------------------------------------------------------------- /client/src/components/users/UserRegisterForm.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 57 | -------------------------------------------------------------------------------- /client/src/locales/en/formats.js: -------------------------------------------------------------------------------- 1 | export default { 2 | dateTime: { 3 | abbr: { 4 | month: 'short', day: '2-digit', 5 | }, 6 | short: { 7 | year: 'numeric', month: 'short', day: '2-digit', 8 | }, 9 | long: { 10 | year: 'numeric', month: 'long', day: '2-digit', 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /client/src/locales/en/index.js: -------------------------------------------------------------------------------- 1 | import messages from './messages' 2 | import formats from './formats' 3 | 4 | export default { 5 | messages, 6 | dateTimeFormats: formats.dateTime, 7 | } 8 | -------------------------------------------------------------------------------- /client/src/locales/fr/formats.js: -------------------------------------------------------------------------------- 1 | export default { 2 | dateTime: { 3 | abbr: { 4 | month: 'short', day: '2-digit', 5 | }, 6 | short: { 7 | year: 'numeric', month: 'short', day: '2-digit', 8 | }, 9 | long: { 10 | year: 'numeric', month: 'long', day: '2-digit', 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /client/src/locales/fr/index.js: -------------------------------------------------------------------------------- 1 | import messages from './messages' 2 | import formats from './formats' 3 | 4 | export default { 5 | messages, 6 | dateTimeFormats: formats.dateTime, 7 | } 8 | -------------------------------------------------------------------------------- /client/src/locales/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | import en from './en' 5 | import fr from './fr' 6 | 7 | Vue.use(VueI18n) 8 | 9 | export const SUPPORTED_LANGUAGES = ['en', 'fr'] 10 | 11 | const i18n = new VueI18n({ 12 | locale: getPreferedLanguage(), 13 | fallbackLocale: 'en', 14 | messages: { 15 | en: en.messages, 16 | fr: fr.messages, 17 | }, 18 | dateTimeFormats: { 19 | en: en.dateTimeFormats, 20 | fr: fr.dateTimeFormats, 21 | }, 22 | }) 23 | 24 | export function getPreferedLanguage () { 25 | const localStorageLanguage = window.localStorage.getItem('language') 26 | if (SUPPORTED_LANGUAGES.includes(localStorageLanguage)) { 27 | return localStorageLanguage 28 | } 29 | const browserLanguage = window.navigator.language 30 | if (SUPPORTED_LANGUAGES.includes(browserLanguage)) { 31 | return browserLanguage 32 | } 33 | return 'en' 34 | } 35 | 36 | export function savePreferedLanguage (language) { 37 | if (SUPPORTED_LANGUAGES.includes(language)) { 38 | window.localStorage.setItem('language', language) 39 | i18n.locale = language 40 | return language 41 | } 42 | } 43 | 44 | export default i18n 45 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import global from './modules/global' 5 | import users from './modules/users' 6 | import features from './modules/features' 7 | import projects from './modules/projects' 8 | import tasks from './modules/tasks' 9 | import termsOfServices from './modules/terms_of_services' 10 | 11 | import cablePlugin from './plugins/cable' 12 | 13 | Vue.use(Vuex) 14 | 15 | const debug = process.env.NODE_ENV !== 'production' 16 | 17 | export default new Vuex.Store({ 18 | modules: { 19 | global, 20 | users, 21 | features, 22 | projects, 23 | tasks, 24 | termsOfServices, 25 | }, 26 | plugins: [ 27 | cablePlugin, 28 | ], 29 | strict: debug, 30 | }) 31 | -------------------------------------------------------------------------------- /client/src/store/modules/features.js: -------------------------------------------------------------------------------- 1 | import api from '@/api/features' 2 | 3 | const state = { 4 | enabled: [], 5 | } 6 | 7 | const getters = { 8 | } 9 | 10 | const actions = { 11 | list ({ commit }) { 12 | return api.list().then((res) => commit('enableList', res.data)) 13 | }, 14 | } 15 | 16 | const mutations = { 17 | enableList (state, data) { 18 | state.enabled = data.map((feature) => feature.id) 19 | }, 20 | } 21 | 22 | export default { 23 | namespaced: true, 24 | state, 25 | getters, 26 | actions, 27 | mutations, 28 | } 29 | -------------------------------------------------------------------------------- /client/src/store/modules/global.js: -------------------------------------------------------------------------------- 1 | import rootApi from '@/api/root' 2 | 3 | const state = { 4 | resourcesReady: false, 5 | registrationDisabled: true, 6 | tosVersion: null, 7 | } 8 | 9 | const getters = { 10 | registrationDisabled (state) { 11 | return state.registrationDisabled 12 | }, 13 | 14 | tosVersion (state) { 15 | return state.tosVersion 16 | }, 17 | 18 | resourcesReady (state) { 19 | return state.resourcesReady 20 | }, 21 | } 22 | 23 | const actions = { 24 | listInfo ({ commit }, email) { 25 | return rootApi.listInfo() 26 | .then((res) => { 27 | commit('setInfo', res) 28 | }) 29 | }, 30 | } 31 | 32 | const mutations = { 33 | setInfo (state, data) { 34 | state.registrationDisabled = data.registrationDisabled 35 | state.tosVersion = data.tosVersion 36 | }, 37 | 38 | setResourcesReady (state, value) { 39 | state.resourcesReady = value 40 | }, 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | getters, 47 | actions, 48 | mutations, 49 | } 50 | -------------------------------------------------------------------------------- /client/src/store/modules/terms_of_services.js: -------------------------------------------------------------------------------- 1 | import termsOfServiceApi from '@/api/terms_of_services' 2 | 3 | const state = { 4 | current: null, 5 | } 6 | 7 | const getters = { 8 | current (state) { 9 | return state.current 10 | }, 11 | } 12 | 13 | const actions = { 14 | getCurrent ({ commit }) { 15 | return termsOfServiceApi.getCurrent() 16 | .then((res) => { 17 | if (res.data) { 18 | commit('setCurrent', res.data) 19 | } 20 | }) 21 | }, 22 | } 23 | 24 | const mutations = { 25 | setCurrent (state, data) { 26 | state.current = { 27 | id: data.id, 28 | ...data.attributes, 29 | } 30 | }, 31 | } 32 | 33 | export default { 34 | namespaced: true, 35 | state, 36 | getters, 37 | actions, 38 | mutations, 39 | } 40 | -------------------------------------------------------------------------------- /client/src/store/plugins/cable.js: -------------------------------------------------------------------------------- 1 | import ActionCable from 'actioncable' 2 | import auth from '@/auth' 3 | 4 | function cablePlugin (store) { 5 | if (!auth.isLoggedIn()) { 6 | return 7 | } 8 | 9 | document.cookie = `Authorization=${auth.getToken()};path=/` 10 | const cable = ActionCable.createConsumer() 11 | cable.subscriptions.create( 12 | { channel: 'NotificationsChannel' }, 13 | { 14 | received: data => { 15 | const [actionType, resourceType] = data.action.split('#', 2) 16 | if (actionType === 'create') { 17 | const resource = store.getters[`${resourceType}/findById`](data.id) 18 | if (!resource) { 19 | store.dispatch(`${resourceType}/get`, { id: data.id }) 20 | } 21 | } else if (actionType === 'update') { 22 | const resource = store.getters[`${resourceType}/findById`](data.id) 23 | if (!resource || resource.updatedAt < data.updatedAt) { 24 | store.dispatch(`${resourceType}/get`, { id: data.id }) 25 | } 26 | } 27 | }, 28 | } 29 | ) 30 | } 31 | 32 | export default cablePlugin 33 | -------------------------------------------------------------------------------- /client/src/styles/_fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Permanent Marker'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Permanent Marker Regular'), 6 | local('PermanentMarker-Regular'), 7 | url('/static/permanentmarker-regular.woff2') format('woff2'); 8 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/styles/_forms.scss: -------------------------------------------------------------------------------- 1 | label { 2 | display: block; 3 | margin-bottom: .25rem; 4 | padding-left: .5rem; 5 | } 6 | 7 | input, 8 | textarea { 9 | width: 100%; 10 | height: 2.5rem; 11 | padding: .5rem; 12 | 13 | color: $ly-color-grey-90; 14 | font-size: 1rem; 15 | line-height: 1.5rem; 16 | font-family: inherit; 17 | 18 | background-color: $ly-color-white; 19 | border: 1px solid $ly-color-grey-40; 20 | box-shadow: 1px 1px 2px $ly-color-grey-20 inset; 21 | border-radius: .25rem; 22 | 23 | transition: border .2s ease-in-out; 24 | 25 | &:hover:not(:disabled) { 26 | box-shadow: 0 0 2px $ly-color-grey-30, 27 | 1px 1px 2px $ly-color-grey-20 inset; 28 | } 29 | &:focus { 30 | border: 1px solid $ly-color-pine-50; 31 | box-shadow: 0 0 1px $ly-color-pine-50, 32 | 1px 1px 2px $ly-color-grey-20 inset; 33 | } 34 | &.invalid { 35 | border: 1px solid $ly-color-red-50; 36 | } 37 | &.invalid:focus { 38 | box-shadow: 0 0 1px $ly-color-red-50, 39 | 1px 1px 2px $ly-color-grey-20 inset; 40 | } 41 | &:disabled { 42 | color: $ly-color-grey-50; 43 | 44 | background-color: $ly-color-grey-20; 45 | border-color: $ly-color-grey-30; 46 | } 47 | 48 | &::placeholder { 49 | color: $ly-color-grey-60; 50 | } 51 | } 52 | 53 | input[type="date"] { 54 | max-width: 10rem; 55 | } 56 | 57 | textarea { 58 | min-height: 7rem; 59 | } 60 | -------------------------------------------------------------------------------- /client/src/styles/_grid.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/src/styles/_grid.scss -------------------------------------------------------------------------------- /client/src/styles/_links.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color: $ly-color-pine-80; 3 | 4 | &:hover, 5 | &:focus { 6 | color: $ly-color-pine-90; 7 | text-decoration: none; 8 | } 9 | } 10 | 11 | .text-on-dark a { 12 | color: $ly-color-white; 13 | 14 | &:hover, 15 | &:focus { 16 | color: $ly-color-grey-30; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 3 | font-size: 20px; 4 | line-height: 1.5rem; 5 | color: $ly-color-grey-90; 6 | } 7 | 8 | @media(max-width: $small-screen-width) { 9 | html { 10 | font-size: 16px; 11 | } 12 | } 13 | 14 | h1, h2, h3 { 15 | margin-top: 0; 16 | margin-bottom: 0.5rem; 17 | 18 | word-wrap: break-word; 19 | } 20 | 21 | h1 { 22 | margin-bottom: 1.75rem; 23 | } 24 | 25 | h1 { font-size: 2rem; line-height: 2.5rem; } 26 | h2 { font-size: 1.5rem; line-height: 2rem; } 27 | h3 { font-size: 1rem; line-height: 1.5rem; } 28 | 29 | * + h1, 30 | * + h2, 31 | * + h3 { 32 | margin-top: 1.5rem; 33 | } 34 | 35 | p { 36 | margin-top: 0; 37 | margin-bottom: 1rem; 38 | } 39 | p:last-child { 40 | margin-bottom: 0; 41 | } 42 | 43 | ul, ol, dl { 44 | margin-top: 0; 45 | margin-bottom: 1rem; 46 | } 47 | ul:last-child, 48 | ol:last-child, 49 | dl:last-child { 50 | margin-bottom: 0; 51 | } 52 | p + ul, 53 | p + ol, 54 | p + dl, 55 | p + table { 56 | margin-top: -.5rem; 57 | } 58 | 59 | small, 60 | .text-addition { 61 | font-size: .8em; 62 | line-height: 1em; 63 | } 64 | 65 | .text-secondary { 66 | color: $ly-color-grey-60; 67 | } 68 | .text-primary { 69 | color: $ly-color-pine-70; 70 | } 71 | .text-success { 72 | color: $ly-color-green-70; 73 | } 74 | .text-warning { 75 | color: $ly-color-gold-80; 76 | } 77 | .text-alert { 78 | color: $ly-color-red-60; 79 | } 80 | 81 | .text-on-dark { 82 | color: $ly-color-white; 83 | } 84 | .text-on-dark.text-secondary { 85 | color: $ly-color-grey-30; 86 | } 87 | 88 | strong, 89 | .text-important { 90 | font-weight: bold; 91 | } 92 | -------------------------------------------------------------------------------- /client/src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import 'variables/index'; 2 | 3 | @import 'fonts'; 4 | @import 'forms'; 5 | @import 'grid'; 6 | @import 'links'; 7 | @import 'typography'; 8 | 9 | @import 'components/vue_tooltip'; 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | html, 16 | body { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | body { 22 | background: url("/static/logo-text-inverse.png") center 25vh no-repeat $ly-color-pine-70; 23 | } 24 | 25 | @media(max-width: $medium-screen-width) { 26 | body { 27 | background: url("/static/logo-inverse.svg") center 25vh no-repeat $ly-color-pine-70; 28 | } 29 | } 30 | 31 | body.modal-opened { 32 | overflow: hidden; 33 | } 34 | 35 | img { 36 | height: auto; 37 | max-width: 100%; 38 | } 39 | 40 | hr { 41 | display: block; 42 | margin-top: 1rem; 43 | margin-bottom: 2rem; 44 | 45 | border: 0; 46 | border-radius: .25rem; 47 | box-shadow: 0 0 0 1px $ly-color-grey-30; 48 | } 49 | 50 | code { 51 | padding: .1rem .25rem; 52 | 53 | font-size: .9em; 54 | 55 | background-color: $ly-color-grey-30; 56 | border-radius: .25rem; 57 | } 58 | .text-on-dark code { 59 | background-color: $ly-color-grey-70; 60 | } 61 | 62 | table { 63 | width: 100%; 64 | margin-bottom: 1rem; 65 | 66 | border-collapse: collapse; 67 | 68 | th { 69 | padding: .25rem .5rem; 70 | 71 | font-size: .9rem; 72 | text-align: left; 73 | font-weight: bold; 74 | 75 | background-color: $ly-color-grey-30; 76 | } 77 | 78 | tr:nth-child(odd) { 79 | background-color: $ly-color-grey-20; 80 | } 81 | 82 | td { 83 | padding: .5rem; 84 | } 85 | } 86 | 87 | @media(max-width: $small-screen-width) { 88 | .no-mobile { 89 | display: none; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /client/src/styles/variables/_dimensions.scss: -------------------------------------------------------------------------------- 1 | $small-screen-width: 800px; 2 | $medium-screen-width: 1100px; 3 | -------------------------------------------------------------------------------- /client/src/styles/variables/_index.scss: -------------------------------------------------------------------------------- 1 | @import "colors.json"; 2 | @import "dimensions"; 3 | -------------------------------------------------------------------------------- /client/src/styles/variables/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "ly-color-white": "#ffffff", 3 | 4 | "ly-color-grey-10": "#f9f9fa", 5 | "ly-color-grey-20": "#ededf0", 6 | "ly-color-grey-30": "#d7d7db", 7 | "ly-color-grey-40": "#b1b1b3", 8 | "ly-color-grey-50": "#737373", 9 | "ly-color-grey-60": "#4a4a4f", 10 | "ly-color-grey-70": "#38383d", 11 | "ly-color-grey-80": "#2a2a2e", 12 | "ly-color-grey-90": "#0c0c0d", 13 | 14 | "ly-color-pine-50": "#00aba8", 15 | "ly-color-pine-60": "#008c8a", 16 | "ly-color-pine-70": "#007876", 17 | "ly-color-pine-80": "#005958", 18 | "ly-color-pine-90": "#004544", 19 | 20 | "ly-color-green-50": "#00a86a", 21 | "ly-color-green-60": "#00945d", 22 | "ly-color-green-70": "#00754a", 23 | "ly-color-green-80": "#005c3a", 24 | "ly-color-green-90": "#00422a", 25 | 26 | "ly-color-gold-50": "#d1b200", 27 | "ly-color-gold-60": "#b29800", 28 | "ly-color-gold-70": "#857100", 29 | "ly-color-gold-80": "#665700", 30 | "ly-color-gold-90": "#524500", 31 | 32 | "ly-color-red-50": "#d40e00", 33 | "ly-color-red-60": "#ab0b00", 34 | "ly-color-red-70": "#880900", 35 | "ly-color-red-80": "#550600", 36 | "ly-color-red-90": "#3b0400" 37 | } 38 | -------------------------------------------------------------------------------- /client/src/utils/array.js: -------------------------------------------------------------------------------- 1 | function mapElementsById (elements, fk = 'id') { 2 | let byIds = {} 3 | elements.forEach((element) => { 4 | const id = element[fk] 5 | byIds[id] = element 6 | }) 7 | return byIds 8 | } 9 | 10 | function groupByFirstCharacter (array, attribute = null) { 11 | let groups = {} 12 | 13 | array.forEach(element => { 14 | const firstCharacter = attribute == null ? element[0] : element[attribute][0] 15 | const group = firstCharacter.match(/[a-z]/i) ? firstCharacter.toUpperCase() : '#' 16 | if (groups[group] == null) { 17 | groups[group] = [] 18 | } 19 | groups[group].push(element) 20 | }) 21 | 22 | return groups 23 | } 24 | 25 | export { 26 | mapElementsById, 27 | groupByFirstCharacter, 28 | } 29 | -------------------------------------------------------------------------------- /client/src/utils/color.js: -------------------------------------------------------------------------------- 1 | function hexToRGB (hexColor) { 2 | if (hexColor.charAt(0) === '#') { 3 | hexColor = hexColor.substring(1) 4 | } 5 | 6 | return [ 7 | parseInt(hexColor.substring(0, 2), 16), 8 | parseInt(hexColor.substring(2, 4), 16), 9 | parseInt(hexColor.substring(4, 6), 16), 10 | ] 11 | } 12 | 13 | function hexLuminance (hexColor) { 14 | return rgbLuminance(hexToRGB(hexColor)) 15 | } 16 | 17 | function rgbLuminance (rgbColor) { 18 | // Reference: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 19 | const [red, green, blue] = rgbColor.map((color8bit) => { 20 | const colorSRGB = color8bit / 255 21 | if (colorSRGB <= 0.03928) { 22 | return colorSRGB / 12.92 23 | } else { 24 | return Math.pow(((colorSRGB + 0.055) / 1.055), 2.4) 25 | } 26 | }) 27 | return 0.2126 * red + 0.7152 * green + 0.0722 * blue 28 | } 29 | 30 | function luminanceRatio (luminance1, luminance2) { 31 | // Reference: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef 32 | if (luminance1 > luminance2) { 33 | return (luminance1 + 0.05) / (luminance2 + 0.05) 34 | } else { 35 | return (luminance2 + 0.05) / (luminance1 + 0.05) 36 | } 37 | } 38 | 39 | export { 40 | hexToRGB, 41 | hexLuminance, 42 | rgbLuminance, 43 | luminanceRatio, 44 | } 45 | -------------------------------------------------------------------------------- /client/src/utils/object.js: -------------------------------------------------------------------------------- 1 | function objectsToOptions (objects, valueKey, labelKey) { 2 | return objects.map(obj => { 3 | return { 4 | value: obj[valueKey], 5 | label: obj[labelKey], 6 | } 7 | }) 8 | } 9 | 10 | export { 11 | objectsToOptions, 12 | } 13 | -------------------------------------------------------------------------------- /client/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/.gitkeep -------------------------------------------------------------------------------- /client/static/back-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/back-home.png -------------------------------------------------------------------------------- /client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/favicon.ico -------------------------------------------------------------------------------- /client/static/font-awesome-4.7.0/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /client/static/font-awesome-4.7.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/font-awesome-4.7.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /client/static/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/icon-256.png -------------------------------------------------------------------------------- /client/static/illustrations/colors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/illustrations/colors.jpg -------------------------------------------------------------------------------- /client/static/illustrations/components.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/illustrations/components.jpg -------------------------------------------------------------------------------- /client/static/illustrations/grid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/illustrations/grid.jpg -------------------------------------------------------------------------------- /client/static/illustrations/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/illustrations/logo.png -------------------------------------------------------------------------------- /client/static/illustrations/typography.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/illustrations/typography.jpg -------------------------------------------------------------------------------- /client/static/illustrations/visuals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/illustrations/visuals.jpg -------------------------------------------------------------------------------- /client/static/illustrations/wording.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/illustrations/wording.jpg -------------------------------------------------------------------------------- /client/static/logo-text-inverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/logo-text-inverse.png -------------------------------------------------------------------------------- /client/static/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/logo-text.png -------------------------------------------------------------------------------- /client/static/permanentmarker-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/client/static/permanentmarker-regular.woff2 -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module Lessy 20 | class Application < Rails::Application 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | config.time_zone = 'UTC' 26 | 27 | # Only loads a smaller set of middleware suitable for API only apps. 28 | # Middleware like session, flash, cookies can be added back manually. 29 | # Skip views, helpers and assets when generating a new resource. 30 | config.api_only = false 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: redis://<%= ENV.fetch 'REDIS_HOST', 'localhost' %>:<%= ENV.fetch 'REDIS_PORT', 6379 %>/1 10 | channel_prefix: lessy_production 11 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: 5 5 | username: <%= ENV.fetch 'DATABASE_USERNAME', 'postgres' %> 6 | password: <%= ENV.fetch 'DATABASE_PASSWORD', 'postgres' %> 7 | host: <%= ENV.fetch 'DATABASE_HOST', 'localhost' %> 8 | post: <%= ENV.fetch 'DATABASE_PORT', 5432 %> 9 | 10 | development: 11 | <<: *default 12 | database: lessy_development 13 | 14 | # Warning: The database defined as "test" will be erased and 15 | # re-generated from your development database when you run "rake". 16 | # Do not set this db to the same as development or production. 17 | test: 18 | <<: *default 19 | database: lessy_test 20 | 21 | production: 22 | <<: *default 23 | database: <%= ENV['DATABASE_NAME'] %> 24 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | 7 | # Make JBuilder lower-cased attributes to be JavaScript-style-compliant 8 | Jbuilder.key_format camelize: :lower 9 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/core_extensions.rb: -------------------------------------------------------------------------------- 1 | Dir[Rails.root.join('app', 'lib', 'core_extensions', '**', '*.rb')].each do |file| 2 | require file 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/flipper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'flipper/adapters/active_record' 4 | 5 | Flipper.configure do |config| 6 | config.default do 7 | adapter = Flipper::Adapters::ActiveRecord.new 8 | Flipper.new(adapter) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/json_param_key_transform.rb: -------------------------------------------------------------------------------- 1 | # From http://stackoverflow.com/a/30557924 2 | # Transform JSON request param keys from JSON-conventional camelCase to 3 | # Rails-conventional snake_case: 4 | ActionDispatch::Request.parameter_parsers[:json] = -> (raw_post) { 5 | # Modified from action_dispatch/http/parameters.rb 6 | data = ActiveSupport::JSON.decode(raw_post) 7 | data = {:_json => data} unless data.is_a?(Hash) 8 | 9 | # Transform camelCase param keys to snake_case: 10 | data.deep_transform_keys!(&:underscore) 11 | } 12 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 8 | # Previous versions had false. 9 | ActiveSupport.to_time_preserves_timezone = true 10 | 11 | # Require `belongs_to` associations by default. Previous versions had false. 12 | Rails.application.config.active_record.belongs_to_required_by_default = true 13 | 14 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 15 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: adec725dac2890c35ab0f4d5d6898282689d7422885848639caf47c657e7fef763846b9ce4f331e865d0bcd732fb679d25c9cb5a503be02e68b041cb1af1da25 15 | 16 | test: 17 | secret_key_base: c985473748adea3e604603a62f3a494b3c486545674f242f64d07779de3e926512c3d475cf30625c1b462530a0a6d446e69d5ac570bd0f9767a6fc848ba76dac 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | smtp_username: <%= ENV.fetch("SMTP_USERNAME") { nil } %> 24 | smtp_password: <%= ENV.fetch("SMTP_PASSWORD") { nil } %> 25 | smtp_address: <%= ENV.fetch("SMTP_ADDRESS") { 'localhost' } %> 26 | smtp_domain: <%= ENV.fetch("SMTP_DOMAIN") { 'localhost.localdomain' } %> 27 | smtp_port: <%= ENV.fetch("SMTP_PORT") { 25 } %> 28 | smtp_authentication: <%= ENV.fetch("SMTP_AUTHENTICATION") { nil } %> 29 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20161226223150_sorcery_core.rb: -------------------------------------------------------------------------------- 1 | class SorceryCore < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :users do |t| 4 | t.string :email, :null => false 5 | t.string :crypted_password 6 | t.string :salt 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :users, :email, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20161226223151_sorcery_user_activation.rb: -------------------------------------------------------------------------------- 1 | class SorceryUserActivation < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :users, :activation_state, :string, :default => nil 4 | add_column :users, :activation_token, :string, :default => nil 5 | add_column :users, :activation_token_expires_at, :datetime, :default => nil 6 | 7 | add_index :users, :activation_token 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20161228214904_add_username_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddUsernameToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :username, :string, null: true 4 | add_index :users, :username, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170109212722_create_projects.rb: -------------------------------------------------------------------------------- 1 | class CreateProjects < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :projects do |t| 4 | t.string :name, null: false 5 | t.references :user, foreign_key: true, null: false 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170113231928_add_description_to_project.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToProject < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :projects, :description, :text, default: '' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170114113847_add_index_on_project_name.rb: -------------------------------------------------------------------------------- 1 | class AddIndexOnProjectName < ActiveRecord::Migration[5.0] 2 | def change 3 | add_index :projects, 'name' 4 | add_index :projects, ['name', 'user_id'], unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170115112412_add_dates_to_project.rb: -------------------------------------------------------------------------------- 1 | class AddDatesToProject < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :projects, :started_at, :datetime 4 | add_column :projects, :due_at, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170116065712_add_finished_at_to_project.rb: -------------------------------------------------------------------------------- 1 | class AddFinishedAtToProject < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :projects, :finished_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170116212138_add_stopped_at_to_project.rb: -------------------------------------------------------------------------------- 1 | class AddStoppedAtToProject < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :projects, :stopped_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170201210318_create_tasks.rb: -------------------------------------------------------------------------------- 1 | class CreateTasks < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :tasks do |t| 4 | t.string :label, null: false 5 | t.datetime :due_at 6 | t.datetime :finished_at 7 | t.references :user, foreign_key: true, null: false 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170529210621_add_abandoned_at_to_task.rb: -------------------------------------------------------------------------------- 1 | class AddAbandonedAtToTask < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :tasks, :abandoned_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170531195512_add_restarted_count_to_task.rb: -------------------------------------------------------------------------------- 1 | class AddRestartedCountToTask < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :tasks, :restarted_count, :integer, null: false, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170601094145_add_order_to_task.rb: -------------------------------------------------------------------------------- 1 | class AddOrderToTask < ActiveRecord::Migration[5.0] 2 | class Task < ApplicationRecord 3 | end 4 | 5 | def up 6 | add_column :tasks, :order, :integer, null: true 7 | 8 | Task.transaction do 9 | User.all.each do |user| 10 | user.tasks.order(:due_at, :label).each_with_index do |task, index| 11 | task.update! order: (index + 1) 12 | end 13 | end 14 | end 15 | 16 | change_column_null :tasks, :order, null: false 17 | add_index :tasks, :order 18 | end 19 | 20 | def down 21 | remove_column :tasks, :order 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20170716102855_add_project_references_to_task.rb: -------------------------------------------------------------------------------- 1 | class AddProjectReferencesToTask < ActiveRecord::Migration[5.0] 2 | def change 3 | add_reference :tasks, :project, foreign_key: true, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171001201530_rename_tasks_restarted_count_in_started_count.rb: -------------------------------------------------------------------------------- 1 | class RenameTasksRestartedCountInStartedCount < ActiveRecord::Migration[5.1] 2 | class Task < ApplicationRecord 3 | end 4 | 5 | def change 6 | rename_column :tasks, :restarted_count, :started_count 7 | Task.update_all('started_count = started_count + 1') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20171006170245_add_state_to_project.rb: -------------------------------------------------------------------------------- 1 | class AddStateToProject < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :projects, :state, :string, null: false, default: 'newed' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171006180251_rename_project_stopped_at_in_paused_at.rb: -------------------------------------------------------------------------------- 1 | class RenameProjectStoppedAtInPausedAt < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :projects, :stopped_at, :paused_at 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171006212950_add_state_to_task.rb: -------------------------------------------------------------------------------- 1 | class AddStateToTask < ActiveRecord::Migration[5.1] 2 | class Task < ApplicationRecord 3 | end 4 | 5 | def up 6 | add_column :tasks, :state, :string, null: false, default: 'newed' 7 | add_column :tasks, :started_at, :datetime 8 | rename_column :tasks, :due_at, :planned_at 9 | rename_column :tasks, :started_count, :planned_count 10 | 11 | Task.update_all "started_at = created_at" 12 | Task.where(planned_at: nil).update_all state: 'started' 13 | Task.where.not(planned_at: nil).update_all state: 'planned' 14 | end 15 | 16 | def down 17 | rename_column :tasks, :planned_count, :started_count 18 | rename_column :tasks, :planned_at, :due_at 19 | remove_column :tasks, :started_at 20 | remove_column :tasks, :state 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20171007004320_set_project_state.rb: -------------------------------------------------------------------------------- 1 | class SetProjectState < ActiveRecord::Migration[5.1] 2 | class Project < ApplicationRecord 3 | end 4 | 5 | def up 6 | Project.where.not(finished_at: nil).update_all state: 'finished' 7 | Project.where(finished_at: nil).where.not(paused_at: nil).update_all state: 'paused' 8 | Project.where(finished_at: nil, paused_at: nil).where.not(started_at: nil).update_all state: 'started' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20171224225137_update_task_state_on_not_started_projects.rb: -------------------------------------------------------------------------------- 1 | class UpdateTaskStateOnNotStartedProjects < ActiveRecord::Migration[5.1] 2 | def up 3 | Project.not_started.find_each do |project| 4 | project.tasks.started.update_all state: 'newed', started_at: nil 5 | end 6 | end 7 | 8 | def down 9 | Task.newed.update_all "state = 'started', started_at = created_at" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20171226101352_add_slug_to_project.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToProject < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :projects, :slug, :string 4 | Project.update_all 'slug = name' 5 | change_column_null :projects, :slug, false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180128172912_add_admin_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAdminToUsers < ActiveRecord::Migration[5.1] 4 | def change 5 | add_column :users, :admin, :boolean, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180205083244_create_flipper_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateFlipperTables < ActiveRecord::Migration[5.1] 4 | def self.up # rubocop:disable Metrics/MethodLength 5 | create_table :flipper_features do |t| 6 | t.string :key, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :flipper_features, :key, unique: true 10 | 11 | create_table :flipper_gates do |t| 12 | t.string :feature_key, null: false 13 | t.string :key, null: false 14 | t.string :value 15 | t.timestamps null: false 16 | end 17 | add_index :flipper_gates, %i[feature_key key value], unique: true 18 | end 19 | 20 | def self.down 21 | drop_table :flipper_gates 22 | drop_table :flipper_features 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /db/migrate/20180617164753_create_feature_registration_feature_flag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateFeatureRegistrationFeatureFlag < ActiveRecord::Migration[5.1] 4 | include FlipperMigration 5 | 6 | def up 7 | create_flag :feature_registration, enabled: true 8 | end 9 | 10 | def down 11 | destroy_flag :feature_registration 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180628165422_create_terms_of_services.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateTermsOfServices < ActiveRecord::Migration[5.1] 4 | def change 5 | create_table :terms_of_services do |t| 6 | t.text :content, null: false 7 | t.string :version, null: false 8 | t.datetime :effective_at, null: false 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180629162317_add_terms_of_service_reference_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTermsOfServiceReferenceToUsers < ActiveRecord::Migration[5.1] 4 | def change 5 | add_reference :users, :terms_of_service, foreign_key: true, null: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180901081437_sorcery_reset_password.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SorceryResetPassword < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :users, :reset_password_token, :string, default: nil 6 | add_column :users, :reset_password_token_expires_at, :datetime, default: nil 7 | add_column :users, :reset_password_email_sent_at, :datetime, default: nil 8 | add_column :users, :access_count_to_reset_password_page, :integer, default: 0 9 | 10 | add_index :users, :reset_password_token 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20190513154840_add_time_zone_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTimeZoneToUsers < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :users, :time_zone, :string, default: 'UTC' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | lessy: 5 | image: lessy:dev 6 | restart: unless-stopped 7 | build: 8 | context: . 9 | dockerfile: Dockerfile.dev 10 | depends_on: 11 | - db 12 | links: 13 | - db:db 14 | ports: 15 | - "3000:3000" 16 | - "5000:5000" 17 | volumes: 18 | - .:/app:z 19 | - bundle:/usr/local/bundle 20 | - node_modules:/app/client/node_modules 21 | environment: 22 | DATABASE_USERNAME: postgres 23 | DATABASE_PASSWORD: postgres 24 | DATABASE_PORT: 5432 25 | DATABASE_HOST: db 26 | 27 | db: 28 | image: postgres 29 | restart: unless-stopped 30 | environment: 31 | POSTGRES_USER: postgres 32 | POSTGRES_PASSWORD: postgres 33 | ports: 34 | - "5432:5432" 35 | 36 | volumes: 37 | bundle: {} 38 | node_modules: {} 39 | -------------------------------------------------------------------------------- /docs/api/authorizations.md: -------------------------------------------------------------------------------- 1 | # Authorizations (API) 2 | 3 | Almost all API requests require authorization which is based on a JsonWebToken 4 | system. We will discuss how to get user token later in this document. 5 | 6 | Important note: unless it is specified, the endpoints requiring authorization 7 | also require that user accepted current terms of service. 8 | 9 | To authorize requests to the Lessy API, you have to send an `Authorization` 10 | HTTP header. 11 | 12 | Example: 13 | 14 | ```console 15 | $ curl -H 'Authorization: ' https://lessy.io/api/users/me 16 | ``` 17 | 18 | Or in JavaScript: 19 | 20 | ```js 21 | window.fetch('https://lessy.io/api/users/me', { 22 | method: 'GET', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | 'Authorization': '', 26 | }, 27 | }); 28 | ``` 29 | 30 | To obtain a token you must authorize yourself: 31 | 32 | ```console 33 | $ curl -H "Content-Type: application/json" \ 34 | -X POST \ 35 | -d '{"username": "dalecooper", "password": "secret"}' \ 36 | https://lessy.io/api/users/authorizations 37 | ``` 38 | 39 | If everything is OK, it should return: 40 | 41 | ```json 42 | { 43 | "data": { 44 | "type": "user", 45 | "id": 1, 46 | "attributes": { 47 | "username": "dalecooper", 48 | "email": "dale.cooper@lessy.io" 49 | } 50 | }, 51 | "meta": { 52 | "token": "" 53 | } 54 | } 55 | ``` 56 | 57 | The returned token must be saved and is valid for the next month only. Then it 58 | will be invalidated. 59 | 60 | You can also ask for a "sudo" token which has greater permissions but is valid 61 | for only 15 minutes. Endpoints requiring a sudo token are clearly identified in 62 | the documentation. 63 | 64 | You can learn more about `users` API in [the dedicated section](users.md). 65 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API overview 2 | 3 | This document describes ressources accessible through Lessy API. It is still 4 | under heavy development, so if you have questions, please open a ticket on [our 5 | bugtracker](https://github.com/lessy-community/lessy/issues/). 6 | 7 | This API only works with JSON and requests SHOULD use HTTPS only. 8 | 9 | Please note all examples are based on lessy.io official service but you MUST 10 | assume that users are not necessarily hosted on this server. 11 | 12 | - [Root API](root.md) 13 | - [Authorizations](authorizations.md) 14 | - [Users](users.md) 15 | - [Projects](projects.md) 16 | - [Tasks](tasks.md) 17 | - [Terms of service](terms_of_service.md) 18 | - [Errors](errors.md) 19 | 20 | Also, a websocket is accessible to be notified of users' task and project 21 | changes. Please refer to [the dedicated documentation](websocket.md) to know 22 | more. 23 | -------------------------------------------------------------------------------- /docs/api/root.md: -------------------------------------------------------------------------------- 1 | # Root (API) 2 | 3 | ## `GET /api` 4 | 5 | Return information about the server. 6 | 7 | **This endpoint doesn't require an `Authorization` header.** 8 | 9 | Parameters: none. 10 | 11 | Result format: 12 | 13 | | Name | Type | Description | Optional | 14 | |----------------------|--------|-------------------------------------------|----------| 15 | | registrationDisabled | bool | Either if registrations are closed or not | | 16 | | tosVersion | string | Version of current terms of service | yes | 17 | 18 | Example: 19 | 20 | ```console 21 | $ curl https://lessy.io/api 22 | ``` 23 | 24 | ```json 25 | { 26 | "registrationDisabled": true, 27 | "tosVersion": "2018-06" 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/api/terms_of_service.md: -------------------------------------------------------------------------------- 1 | # Terms of service (API) 2 | 3 | ## `GET /api/terms_of_services/current` 4 | 5 | Return the current terms applicable to the service. 6 | 7 | **This endpoint doesn't require an `Authorization` header.** 8 | 9 | Parameters: none. 10 | 11 | Result format: 12 | 13 | | Name | Type | Description | Optional | 14 | |-----------------------------|--------|---------------------------------------------------|----------| 15 | | data | object | | yes | 16 | | data.type | string | Type of returned data (always `terms_of_service`) | | 17 | | data.id | number | Terms of service's identifier | | 18 | | data.attributes | object | | | 19 | | data.attributes.content | string | Terms of service's content (HTML) | | 20 | | data.attributes.version | string | Terms of service's version (unique) | | 21 | | data.attributes.effectiveAt | number | Since when the terms apply to the service | | 22 | 23 | Note: the endpoint can return a `no_content` response if there is no terms of 24 | service. 25 | 26 | Example: 27 | 28 | ```console 29 | $ curl https://lessy.io/api/terms_of_services/current 30 | ``` 31 | 32 | ```json 33 | { 34 | "data": { 35 | "type": "terms_of_service", 36 | "id": 1, 37 | "attributes": { 38 | "content": "[...]", 39 | "version": "2018-06", 40 | "effectiveAt": 1529855818 41 | } 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/backend/index.md: -------------------------------------------------------------------------------- 1 | # Backend overview 2 | 3 | Lessy's backend is a very basic Ruby on Rails application, nothing fancy. If 4 | you don't know about Rails, please start by their ["getting started" guide](http://guides.rubyonrails.org/getting_started.html). 5 | It will guide you over all you need to know to start to help us. 6 | 7 | Some points need a specific attention though, there are detailed in the 8 | following documents: 9 | 10 | - [lifecycle and state machine](lifecycle_and_state_machine.md) 11 | - [endpoints' design](endpoints_design.md) 12 | - [writing tests](writing_tests.md) 13 | -------------------------------------------------------------------------------- /docs/frontend/index.md: -------------------------------------------------------------------------------- 1 | # Frontend overview 2 | 3 | Lessy's frontend is written with VueJS and is built with NPM and Webpack. You 4 | can find the code under the `client/` directory. Please refer to the [VueJS 5 | guide](https://vuejs.org/v2/guide/) to get started with it. 6 | 7 | Our design guide is accessible at [lessy.io/design](https://lessy.io/design), 8 | please always refer to it when working on the frontend. 9 | 10 | We describe general guidelines to test the frontend in [a specific 11 | document](writing_tests.md). 12 | 13 | This documentation is still under construction. Later, we will document: 14 | 15 | - what is and how to write generic components 16 | - how to organize components 17 | - how to write good store's module 18 | - localisation 19 | -------------------------------------------------------------------------------- /docs/frontend/writing_tests.md: -------------------------------------------------------------------------------- 1 | # Writing tests (frontend) 2 | 3 | We use [Jest](http://facebook.github.io/jest/) + [vue-test-utils](https://vue-test-utils.vuejs.org/en/) 4 | for our frontend tests. For the moment, the frontend suite test is very basic 5 | but we strongly encourage you to write tests when adding new features. 6 | 7 | Tests are written under `client/spec` directory. 8 | 9 | And, yes, we know: testing the frontend is never easy nor pleasant. But let's 10 | try! 11 | 12 | ## What to test? 13 | 14 | Actually, we only test presentational components: there are very basic (few 15 | JavaScript interactions) and they do not require complex mocking. These tests 16 | are not very interesting since they are already taking more than 10 seconds for 17 | less than 20 tests and the code which is tested is extremely simple! However, 18 | they gave insight of what is possible to test. 19 | 20 | Later, we would like to test container components wrapping these components 21 | since they handle more business logic. Store might be tested as well. 22 | 23 | However, we lack of examples for the moment. If you have any question or 24 | suggestion, don't hesitate to [open a ticket on GitHub](https://github.com/lessy-community/lessy/issues). 25 | -------------------------------------------------------------------------------- /docs/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes # 2 | 3 | Changes proposed in this pull request: 4 | 5 | - 6 | - 7 | - 8 | 9 | Pull request checklist: 10 | 11 | - [ ] branch is rebased on `master` \* 12 | - [ ] proper commit messages \* 13 | - [ ] proper coding style \* 14 | - [ ] code is properly tested 15 | - [ ] document API changes both in [technical documentation](https://github.com/lessy-community/lessy/tree/master/docs/api) and [changelog](https://github.com/lessy-community/lessy/blob/master/CHANGELOG.md) (optional) 16 | - [ ] [document migration notes](https://github.com/lessy-community/lessy/blob/master/CHANGELOG.md) (optional) 17 | - [ ] reviewer assigned (@marienfressinaud) 18 | 19 | \* [Additional information in the documentation](https://github.com/lessy-community/lessy/tree/master/docs/pull_request.md). 20 | -------------------------------------------------------------------------------- /docs/screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/docs/screenshots/dashboard.png -------------------------------------------------------------------------------- /docs/tests.md: -------------------------------------------------------------------------------- 1 | # Running tests 2 | 3 | In this section, we assume you already have installed development environment 4 | and you are running it on Docker. 5 | 6 | Tests are using [RSpec](http://rspec.info/) (backend) and [Jest](http://facebook.github.io/jest/) + 7 | [vue-test-utils](https://vue-test-utils.vuejs.org/en/). 8 | 9 | To execute them, just run: 10 | 11 | ```console 12 | $ make test 13 | $ # or 14 | $ make test-back 15 | $ make test-front 16 | ``` 17 | 18 | This command calls `rspec` and `jest` commands with docker-compose and it 19 | passes `docker-compose-test.yml` in argument which is an adapted and simpler 20 | version of `docker-compose-dev.yml`. 21 | 22 | Tests' suite is run against [TravisCI](https://travis-ci.org/lessy-community/lessy). 23 | Pull requests must pass tests to be merged so please make sure it's all green 24 | before asking for a review (but you still can ask for help!) 25 | 26 | If you want to know more about how to write tests, please have a look to the 27 | dedicated document [for backend](backend/writing_tests.md) and [for 28 | frontend](frontend/writing_tests.md). 29 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/log/.keep -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/factories/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/spec/factories/.keep -------------------------------------------------------------------------------- /spec/factories/projects.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :project, traits: [:newed] do 3 | sequence(:name, 'a') { |n| "My project #{n}" } 4 | user 5 | 6 | trait :newed do 7 | state { 'newed' } 8 | started_at { nil } 9 | paused_at { nil } 10 | due_at { nil } 11 | finished_at { nil } 12 | end 13 | 14 | trait :started do 15 | state { 'started' } 16 | started_at { 15.days.ago } 17 | paused_at { nil } 18 | due_at { 15.days.from_now } 19 | finished_at { nil } 20 | end 21 | 22 | trait :paused do 23 | state { 'paused' } 24 | started_at { 30.days.ago } 25 | paused_at { 15.days.ago } 26 | due_at { 15.days.from_now } 27 | finished_at { nil } 28 | end 29 | 30 | trait :finished do 31 | state { 'finished' } 32 | started_at { 30.days.ago } 33 | paused_at { nil } 34 | due_at { 15.days.ago } 35 | finished_at { 15.days.ago } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/factories/tasks.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :task, traits: [:newed] do 3 | label { 'My task' } 4 | user 5 | sequence :order 6 | 7 | trait :newed do 8 | state { 'newed' } 9 | started_at { nil } 10 | planned_at { nil } 11 | finished_at { nil } 12 | abandoned_at { nil } 13 | end 14 | 15 | trait :started do 16 | state { 'started' } 17 | started_at { 15.days.ago } 18 | planned_at { nil } 19 | finished_at { nil } 20 | abandoned_at { nil } 21 | end 22 | 23 | trait :planned do 24 | state { 'planned' } 25 | started_at { 15.days.ago } 26 | planned_at { 15.days.from_now } 27 | finished_at { nil } 28 | abandoned_at { nil } 29 | end 30 | 31 | trait :finished do 32 | state { 'finished' } 33 | planned_at { 15.days.ago } 34 | finished_at { 5.days.ago } 35 | abandoned_at { nil } 36 | end 37 | 38 | trait :abandoned do 39 | state { 'abandoned' } 40 | finished_at { nil } 41 | abandoned_at { 5.days.ago } 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/factories/terms_of_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :terms_of_service do 5 | content { 'ToS content' } 6 | sequence(:version) { |i| "2018-#{i}" } 7 | effective_at { 1.day.ago } 8 | 9 | trait :in_the_past do 10 | effective_at { 1.month.ago } 11 | end 12 | 13 | trait :in_the_future do 14 | effective_at { 1.month.from_now } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | sequence(:email) { |n| "john+#{ n }@doe.com" } 4 | sequence(:username, 'a') { |n| "john_#{ n }" } 5 | 6 | trait :activated do 7 | after(:create) do |user| 8 | user.activate! 9 | end 10 | end 11 | 12 | trait :inactive do 13 | # nothing on purpose 14 | end 15 | 16 | trait :password_reseted do 17 | after(:create, &:generate_reset_password_token!) 18 | end 19 | 20 | trait :not_accepted_tos do 21 | terms_of_service { nil } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/json_web_token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'base64' 3 | 4 | RSpec.describe JsonWebToken do 5 | describe '.encode' do 6 | let(:token) { described_class.encode({ foo: 'bar' }, 1.day.from_now) } 7 | 8 | it 'returns a token containing periods' do 9 | expect(token).to include('.') 10 | end 11 | 12 | it 'returns a token where each part is a valid base64-encoded value' do 13 | token.split('.').each do |part| 14 | expect { Base64.urlsafe_decode64(part) }.not_to raise_error 15 | end 16 | end 17 | end 18 | 19 | describe '.decode' do 20 | let(:decoded_token) { described_class.decode(token) } 21 | 22 | context 'when expiration is in the future' do 23 | let(:token) { described_class.encode({ foo: 'bar' }, 1.day.from_now) } 24 | 25 | it 'returns a decoded token with passed data' do 26 | expect(decoded_token[:data][:foo]).to eq('bar') 27 | end 28 | end 29 | 30 | context 'when expiration is in the past' do 31 | let(:token) { described_class.encode({ foo: 'bar' }, 1.day.ago) } 32 | 33 | it 'returns nil' do 34 | expect(decoded_token).to be_nil 35 | end 36 | end 37 | 38 | context 'when token is nil' do 39 | let(:token) { nil } 40 | 41 | it 'returns nil' do 42 | expect(decoded_token).to be_nil 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/mailers/user_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe UserMailer do 4 | describe 'activation_needed_email' do 5 | let(:user) { create :user } 6 | let(:mail) { described_class.activation_needed_email(user).deliver_now } 7 | 8 | it 'renders the subject' do 9 | expect(mail.subject).to eq('[Lessy] Welcome to Lessy!') 10 | end 11 | 12 | it 'renders the receiver email' do 13 | expect(mail.to).to eq([user.email]) 14 | end 15 | 16 | it 'renders the sender email' do 17 | expect(mail.from).to eq(['noreply@lessy.io']) 18 | end 19 | 20 | it 'renders the user activation link' do 21 | expect(mail.body.encoded).to match(user.activation_token) 22 | end 23 | end 24 | 25 | describe 'activation_success_email' do 26 | let(:user) { create :user, username: 'john' } 27 | let(:mail) { described_class.activation_success_email(user).deliver_now } 28 | 29 | it 'renders the subject' do 30 | expect(mail.subject).to eq('[Lessy] Your account is now activated') 31 | end 32 | 33 | it 'renders the receiver email' do 34 | expect(mail.to).to eq([user.email]) 35 | end 36 | 37 | it 'renders the sender email' do 38 | expect(mail.from).to eq(['noreply@lessy.io']) 39 | end 40 | 41 | it 'renders the sender username' do 42 | expect(mail.body.encoded).to match(user.username) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/requests/api/terms_of_services_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe Api::TermsOfServicesController, type: :request do 6 | describe 'GET #current' do 7 | subject { get api_terms_of_services_current_path } 8 | 9 | context 'with no terms of service' do 10 | before do 11 | TermsOfService.destroy_all 12 | subject 13 | end 14 | 15 | it 'succeeds with no content' do 16 | expect(response).to have_http_status(:no_content) 17 | end 18 | end 19 | 20 | context 'with actual terms of service' do 21 | let!(:actual_tos) { create :terms_of_service, :in_the_past } 22 | let!(:next_tos) { create :terms_of_service, :in_the_future } 23 | 24 | before { subject } 25 | 26 | it 'succeeds' do 27 | expect(response).to have_http_status(:ok) 28 | end 29 | 30 | it 'matches the terms_of_services/current schema' do 31 | expect(response).to match_response_schema('terms_of_services/current') 32 | end 33 | 34 | it 'returns the actual terms of service' do 35 | tos_json = JSON.parse(response.body)['data'] 36 | expect(tos_json['id']).to eq(actual_tos.id) 37 | expect(tos_json['attributes']['content']).to eq(actual_tos.content) 38 | expect(tos_json['attributes']['version']).to eq(actual_tos.version) 39 | expect(tos_json['attributes']['effectiveAt']).to eq(actual_tos.effective_at.to_i) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/shared_examples_for_failures.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_examples 'API errors' do |http_status, error_body| 2 | it 'fails' do 3 | expect(response).to have_http_status(http_status) 4 | end 5 | 6 | it 'matches errors.json schema' do 7 | expect(response).to match_response_schema('errors') 8 | end 9 | 10 | it 'returns errors' do 11 | body = JSON.parse(response.body, symbolize_names: true) 12 | expect(body).to eq(error_body) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/api/schemas/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["errors"], 4 | "properties": { 5 | "errors": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "required": ["status", "code", "title", "detail"], 10 | "properties": { 11 | "status": { "type": "string" }, 12 | "code": { "type": "string" }, 13 | "title": { "type": "string" }, 14 | "detail": { "type": "string" }, 15 | "source": { 16 | "type": "object", 17 | "required": ["pointer"], 18 | "properties": { 19 | "pointer": { "type": "string" } 20 | }, 21 | "additionalProperties": false 22 | } 23 | }, 24 | "additionalProperties": false 25 | } 26 | } 27 | }, 28 | "additionalProperties": false 29 | } 30 | -------------------------------------------------------------------------------- /spec/support/api/schemas/features/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "required": ["id", "type"], 10 | "properties": { 11 | "type": { "type": "string" }, 12 | "id": { "type": "string" } 13 | }, 14 | "additionalProperties": false 15 | } 16 | } 17 | }, 18 | "additionalProperties": false 19 | } 20 | -------------------------------------------------------------------------------- /spec/support/api/schemas/projects/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "project.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/projects/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data", "links"], 4 | "properties": { 5 | "data": { 6 | "type": "array", 7 | "items": { "$ref": "project.json" } 8 | }, 9 | "links": { 10 | "type": "object", 11 | "required": ["first", "last"], 12 | "properties": { 13 | "first": { "type": "string" }, 14 | "last": { "type": "string" }, 15 | "prev": { "type": "string" }, 16 | "next": { "type": "string" } 17 | }, 18 | "additionalProperties": false 19 | } 20 | }, 21 | "additionalProperties": false 22 | } 23 | -------------------------------------------------------------------------------- /spec/support/api/schemas/projects/show.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "project.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/projects/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "project.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/projects/update_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "project.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/tasks/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "task.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/tasks/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data", "links"], 4 | "properties": { 5 | "data": { 6 | "type": "array", 7 | "items": { "$ref": "task.json" } 8 | }, 9 | "links": { 10 | "type": "object", 11 | "required": ["first", "last"], 12 | "properties": { 13 | "first": { "type": "string" }, 14 | "last": { "type": "string" }, 15 | "prev": { "type": "string" }, 16 | "next": { "type": "string" } 17 | }, 18 | "additionalProperties": false 19 | } 20 | }, 21 | "additionalProperties": false 22 | } 23 | -------------------------------------------------------------------------------- /spec/support/api/schemas/tasks/show.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "task.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/tasks/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "task.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/tasks/update_order.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "required": ["type", "id", "attributes"], 10 | "properties": { 11 | "type": { "type": "string" }, 12 | "id": { "type": "integer" }, 13 | "attributes": { 14 | "type": "object", 15 | "required": ["order"], 16 | "properties": { 17 | "order": { "type": "integer" } 18 | }, 19 | "additionalProperties": false 20 | } 21 | }, 22 | "additionalProperties": false 23 | } 24 | } 25 | }, 26 | "additionalProperties": false 27 | } 28 | -------------------------------------------------------------------------------- /spec/support/api/schemas/tasks/update_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "task.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/terms_of_services/current.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { 6 | "type": "object", 7 | "required": ["id", "type", "attributes"], 8 | "properties": { 9 | "id": { "type": "integer" }, 10 | "type": { "type": "string" }, 11 | "attributes": { 12 | "type": "object", 13 | "required": ["content", "version", "effectiveAt"], 14 | "properties": { 15 | "content": { "type": "string" }, 16 | "version": { "type": "string" }, 17 | "effectiveAt": { "type": "integer" } 18 | }, 19 | "additionalProperties": false 20 | } 21 | }, 22 | "additionalProperties": false 23 | } 24 | }, 25 | "additionalProperties": false 26 | } 27 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/accept_tos.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { 6 | "type": "object", 7 | "required": ["id", "type", "attributes"], 8 | "properties": { 9 | "id": { "type": "integer" }, 10 | "type": { "type": "string" }, 11 | "attributes": { 12 | "type": "object", 13 | "required": ["hasAcceptedTos"], 14 | "properties": { 15 | "hasAcceptedTos": { "type": "boolean" } 16 | }, 17 | "additionalProperties": false 18 | } 19 | }, 20 | "additionalProperties": false 21 | } 22 | }, 23 | "additionalProperties": false 24 | } 25 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/activations/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data", "meta"], 4 | "properties": { 5 | "data": { "$ref": "../user.json" }, 6 | "meta": { 7 | "type": "object", 8 | "required": ["token"], 9 | "properties": { 10 | "token": { "type": "string" } 11 | }, 12 | "additionalProperties": false 13 | } 14 | }, 15 | "additionalProperties": false 16 | } 17 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/authorizations/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data", "meta"], 4 | "properties": { 5 | "data": { "$ref": "../user.json" }, 6 | "meta": { 7 | "type": "object", 8 | "required": ["token"], 9 | "properties": { 10 | "token": { "type": "string" } 11 | }, 12 | "additionalProperties": false 13 | } 14 | }, 15 | "additionalProperties": false 16 | } 17 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data", "meta"], 4 | "properties": { 5 | "data": { "$ref": "user.json" }, 6 | "meta": { 7 | "type": "object", 8 | "required": ["token"], 9 | "properties": { 10 | "token": { "type": "string" } 11 | }, 12 | "additionalProperties": false 13 | } 14 | }, 15 | "additionalProperties": false 16 | } 17 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/me.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "user.json" } 6 | }, 7 | "additionalProperties": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/passwords/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { "$ref": "../user.json" }, 6 | "meta": { 7 | "type": "object", 8 | "required": ["token"], 9 | "properties": { 10 | "token": { "type": "string" } 11 | }, 12 | "additionalProperties": false 13 | } 14 | }, 15 | "additionalProperties": false 16 | } 17 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["data"], 4 | "properties": { 5 | "data": { 6 | "type": "object", 7 | "required": ["id", "type", "attributes"], 8 | "properties": { 9 | "id": { "type": "integer" }, 10 | "type": { "type": "string" }, 11 | "attributes": { 12 | "type": "object", 13 | "required": ["email", "username", "timeZone"], 14 | "properties": { 15 | "email": { "type": "string" }, 16 | "username": { "type": "string" }, 17 | "timeZone": { "type": "string" } 18 | }, 19 | "additionalProperties": false 20 | } 21 | }, 22 | "additionalProperties": false 23 | } 24 | }, 25 | "additionalProperties": false 26 | } 27 | -------------------------------------------------------------------------------- /spec/support/api/schemas/users/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["id", "type", "attributes"], 4 | "properties": { 5 | "id": { "type": "integer" }, 6 | "type": { "type": "string" }, 7 | "attributes": { 8 | "type": "object", 9 | "required": ["email", "hasAcceptedTos", "timeZone"], 10 | "properties": { 11 | "email": { "type": "string" }, 12 | "username": { 13 | "oneOf": [ 14 | { "type": "string" }, 15 | { "type": "null" } 16 | ] 17 | }, 18 | "admin": { "type": "boolean" }, 19 | "hasAcceptedTos": { "type": "boolean" }, 20 | "timeZone": { "type": "string" } 21 | }, 22 | "additionalProperties": false 23 | } 24 | }, 25 | "additionalProperties": false 26 | } 27 | -------------------------------------------------------------------------------- /spec/support/api/schemas/welcome/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["registrationDisabled"], 4 | "properties": { 5 | "registrationDisabled": { "type": "boolean" }, 6 | "tosVersion": { "type": "string" } 7 | }, 8 | "additionalProperties": false 9 | } 10 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryBot::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/json_matchers.rb: -------------------------------------------------------------------------------- 1 | JsonMatchers.configure do |config| 2 | config.options[:strict] = true 3 | end 4 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lessy-community/lessy/5cd1846eb96a835d6177546b726b38ab6b3f4234/tmp/.keep --------------------------------------------------------------------------------