.crystal-version CHANGED
@@ -1 +1 @@
1
- 1.5.1
1
+ 1.7.3
.github/workflows/ci.yml CHANGED
@@ -1,54 +1,54 @@
1
1
  name: my_app CI
2
2
 
3
3
  on:
4
4
  push:
5
5
  branches: "*"
6
6
  pull_request:
7
7
  branches: "*"
8
8
 
9
9
  jobs:
10
10
  check-format:
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
14
  crystal_version:
15
- - 1.5.1
15
+ - 1.7.3
16
16
  experimental:
17
17
  - false
18
18
  runs-on: ubuntu-latest
19
19
  continue-on-error: ${{ matrix.experimental }}
20
20
  steps:
21
21
  - uses: actions/checkout@v2
22
22
  - name: Install Crystal
23
23
  uses: crystal-lang/install-crystal@v1
24
24
  with:
25
25
  crystal: ${{ matrix.crystal_version }}
26
26
  - name: Format
27
27
  run: crystal tool format --check
28
28
 
29
29
  specs:
30
30
  strategy:
31
31
  fail-fast: false
32
32
  matrix:
33
33
  crystal_version:
34
- - 1.5.1
34
+ - 1.7.3
35
35
  experimental:
36
36
  - false
37
37
  runs-on: ubuntu-latest
38
38
  env:
39
39
  LUCKY_ENV: test
40
40
  DB_HOST: localhost
41
41
  continue-on-error: ${{ matrix.experimental }}
42
42
  services:
43
43
  postgres:
44
44
  image: postgres:12-alpine
45
45
  env:
46
46
  POSTGRES_PASSWORD: postgres
47
47
  ports:
48
48
  - 5432:5432
49
49
  # Set health checks to wait until postgres has started
50
50
  options: >-
51
51
  --health-cmd pg_isready
52
52
  --health-interval 10s
53
53
  --health-timeout 5s
54
54
  --health-retries 5
README.md CHANGED
@@ -1,14 +1,23 @@
1
1
  # my_app
2
2
 
3
3
  This is a project written using [Lucky](https://luckyframework.org). Enjoy!
4
4
 
5
5
  ### Setting up the project
6
6
 
7
7
  1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies)
8
8
  1. Update database settings in `config/database.cr`
9
9
  1. Run `script/setup`
10
10
  1. Run `lucky dev` to start the app
11
11
 
12
+ ### Using Docker for development
13
+
14
+ 1. [Install Docker](https://docs.docker.com/engine/install/)
15
+ 1. Run `docker compose up`
16
+
17
+ The Docker container will boot all of the necessary components needed to run your Lucky application.
18
+ To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file.
19
+
20
+
12
21
  ### Learning Lucky
13
22
 
14
23
  Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky).
docker/dev_entrypoint.sh CHANGED
@@ -1,53 +1,46 @@
1
1
  #!/bin/bash
2
2
 
3
3
  set -euo pipefail
4
4
 
5
- # This is the entrypoint script used for development docker workflows. By
5
+ # This is the entrypoint script used for development docker workflows.
6
- # default it will:
6
+ # By default it will:
7
- # - Install shards.
7
+ # - Install dependencies.
8
8
  # - Run migrations.
9
9
  # - Start the dev server.
10
10
  # It also accepts any commands to be run instead.
11
11
 
12
12
 
13
13
  warnfail () {
14
14
  echo "$@" >&2
15
15
  exit 1
16
16
  }
17
17
 
18
18
  case ${1:-} in
19
19
  "") # If no arguments are provided, start lucky dev server.
20
20
  ;;
21
21
 
22
22
  *) # If any arguments are provided, execute them instead.
23
23
  exec "$@"
24
24
  esac
25
25
 
26
26
  if ! [ -d bin ] ; then
27
27
  echo "Creating bin directory"
28
28
  mkdir bin
29
29
  fi
30
-
31
30
  echo "Installing npm packages..."
32
31
  yarn install
33
-
34
32
  if ! shards check ; then
35
33
  echo "Installing shards..."
36
34
  shards install
37
35
  fi
38
36
 
39
37
  echo "Waiting for postgres to be available..."
40
38
  ./docker/wait-for-it.sh -q postgres:5432
41
39
 
42
40
  if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then
43
41
  echo "Finishing database setup..."
44
42
  lucky db.migrate
45
43
  fi
46
44
 
47
- if [ -S .overmind.sock ] ; then
48
- echo "Removing old overmind socket file..."
49
- rm .overmind.sock
50
- fi
51
-
52
45
  echo "Starting lucky dev server..."
53
46
  exec lucky dev
docker/development.dockerfile CHANGED
@@ -1,40 +1,34 @@
1
- FROM crystallang/crystal:1.4.1
1
+ FROM crystallang/crystal:1.7.3
2
2
 
3
3
  # Install utilities required to make this Dockerfile run
4
4
  RUN apt-get update && \
5
- apt-get install -y wget gunzip
5
+ apt-get install -y wget
6
-
7
6
  # Add the nodesource ppa to apt. Update this to change the nodejs version.
8
7
  RUN wget https://deb.nodesource.com/setup_16.x -O- | bash
9
8
 
10
9
  # Apt installs:
11
10
  # - nodejs (from above ppa) is required for front-end apps.
12
11
  # - Postgres cli tools are required for lucky-cli.
13
12
  # - tmux is required for the Overmind process manager.
14
13
  RUN apt-get update && \
15
14
  apt-get install -y nodejs postgresql-client tmux && \
16
15
  rm -rf /var/lib/apt/lists/*
17
16
 
18
17
  # NPM global installs:
19
18
  # - Yarn is the default package manager for the node component of a lucky
20
19
  # browser app.
21
20
  # - Mix is the default asset compiler.
22
21
  RUN npm install -g yarn mix
23
22
 
24
- # Installs overmind, not needed if nox is the process manager.
25
- RUN wget https://github.com/DarthSim/overmind/releases/download/v2.2.2/overmind-v2.2.2-linux-amd64.gz && \
26
- gunzip overmind-v2.2.2-linux-amd64.gz && \
27
- mv overmind-v2.2.2-linux-amd64 /usr/bin/overmind && \
28
- chmod +x /usr/bin/overmind
29
-
30
- # Install lucky cli, TODO: fetch current lucky version from source code.
23
+ # Install lucky cli
31
24
  WORKDIR /lucky/cli
32
25
  RUN git clone https://github.com/luckyframework/lucky_cli . && \
33
- git checkout v0.30.0 && \
26
+ git checkout v1.0.0 && \
34
27
  shards build --without-development && \
35
28
  cp bin/lucky /usr/bin
36
29
 
37
30
  WORKDIR /app
38
31
  ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres
32
+ EXPOSE 3000
39
- EXPOSE 5001
33
+ EXPOSE 3001
40
34
 
docker-compose.yml CHANGED
@@ -1,42 +1,44 @@
1
1
  version: '3.8'
2
2
  services:
3
3
  lucky:
4
4
  build:
5
5
  context: .
6
6
  dockerfile: docker/development.dockerfile
7
7
  environment:
8
8
  DATABASE_URL: postgres://lucky:password@postgres:5432/lucky
9
+ DEV_HOST: "0.0.0.0"
9
10
  volumes:
10
11
  - type: bind
11
12
  source: .
12
13
  target: /app
13
14
  - type: volume
14
15
  source: node_modules
15
16
  target: /app/node_modules
16
17
  - type: volume
17
18
  source: shards_lib
18
19
  target: /app/lib
19
20
  depends_on:
20
21
  - postgres
21
22
  ports:
22
- - 5001:5001
23
+ - 3000:3000 # This is the Lucky Server port
24
+ - 3001:3001 # This is the Lucky watcher reload port
23
25
 
24
26
  entrypoint: ["docker/dev_entrypoint.sh"]
25
27
 
26
28
  postgres:
27
29
  image: postgres:14-alpine
28
30
  environment:
29
31
  POSTGRES_USER: lucky
30
32
  POSTGRES_PASSWORD: password
31
33
  volumes:
32
34
  - type: volume
33
35
  source: postgres_data
34
36
  target: /var/lib/postgresql
35
37
  ports:
36
38
  # The postgres database container is exposed on the host at port 6543 to
37
39
  # allow connecting directly to it with postgres clients. The port differs
38
40
  # from the postgres default to avoid conflict with existing postgres
39
41
  # servers. Connect to a running postgres container with:
40
42
  # postgres://lucky:password@localhost:6543/lucky
41
43
  - 6543:5432
42
44
 
shard.yml CHANGED
@@ -1,41 +1,41 @@
1
1
  name: my_app
2
2
  version: 0.1.0
3
3
 
4
4
  authors:
5
5
  - your-name-here <your-email-here>
6
6
 
7
7
  targets:
8
8
  app:
9
9
  main: src/my_app.cr
10
10
 
11
- crystal: 1.5.1
11
+ crystal: 1.7.3
12
12
 
13
13
  dependencies:
14
14
  lucky:
15
15
  github: luckyframework/lucky
16
- version: ~> 1.0.0-rc1
16
+ version: ~> 1.0.0
17
17
  avram:
18
18
  github: luckyframework/avram
19
- version: ~> 1.0.0-rc1
19
+ version: ~> 1.0.0
20
20
  carbon:
21
21
  github: luckyframework/carbon
22
22
  version: ~> 0.3.0
23
23
  carbon_sendgrid_adapter:
24
24
  github: luckyframework/carbon_sendgrid_adapter
25
25
  version: ~> 0.3.0
26
26
  lucky_env:
27
27
  github: luckyframework/lucky_env
28
28
  version: ~> 0.1.4
29
29
  lucky_task:
30
30
  github: luckyframework/lucky_task
31
31
  version: ~> 0.1.1
32
32
  authentic:
33
33
  github: luckyframework/authentic
34
- version: ~> 0.9.0
34
+ version: ~> 1.0.0
35
35
  jwt:
36
36
  github: crystal-community/jwt
37
37
  version: ~> 1.6.0
38
38
  development_dependencies:
39
39
  lucky_flow:
40
40
  github: luckyframework/lucky_flow
41
41
  version: ~> 0.9.0
spec/support/flows/reset_password_flow.cr CHANGED
@@ -17,26 +17,26 @@
17
17
  end
18
18
  end
19
19
 
20
20
  def should_have_sent_reset_email
21
21
  with_fake_token do
22
22
  user = UserQuery.new.email(email).first
23
23
  PasswordResetRequestEmail.new(user).should be_delivered
24
24
  end
25
25
  end
26
26
 
27
27
  def reset_password(password)
28
28
  user = UserQuery.new.email(email).first
29
29
  token = Authentic.generate_password_reset_token(user)
30
30
  visit PasswordResets::New.with(user.id, token)
31
31
  fill_form ResetPassword,
32
32
  password: password,
33
33
  password_confirmation: password
34
34
  click "@update-password-button"
35
35
  end
36
36
 
37
- private def with_fake_token
37
+ private def with_fake_token(&)
38
38
  PasswordResetRequestEmail.temp_config(stubbed_token: "fake") do
39
39
  yield
40
40
  end
41
41
  end
42
42
  end
src/actions/browser_action.cr CHANGED
@@ -21,24 +21,25 @@
21
21
 
22
22
  # When testing you can skip normal sign in by using `visit` with the `as` param
23
23
  #
24
24
  # flow.visit Me::Show, as: UserFactory.create
25
25
  include Auth::TestBackdoor
26
26
 
27
27
  # By default all actions that inherit 'BrowserAction' require sign in.
28
28
  #
29
29
  # You can remove the 'include Auth::RequireSignIn' below to allow anyone to
30
30
  # access actions that inherit from 'BrowserAction' or you can
31
31
  # 'include Auth::AllowGuests' in individual actions to skip sign in.
32
32
  include Auth::RequireSignIn
33
33
 
34
34
  # `expose` means that `current_user` will be passed to pages automatically.
35
35
  #
36
36
  # In default Lucky apps, the `MainLayout` declares it `needs current_user : User`
37
37
  # so that any page that inherits from MainLayout can use the `current_user`
38
38
  expose current_user
39
39
 
40
40
  # This method tells Authentic how to find the current user
41
+ # The 'memoize' macro makes sure only one query is issued to find the user
41
- private def find_current_user(id) : User?
42
+ private memoize def find_current_user(id : String | User::PrimaryKeyType) : User?
42
43
  UserQuery.new.id(id).first?
43
44
  end
44
45
  end
src/actions/mixins/api/auth/helpers.cr CHANGED
@@ -1,22 +1,23 @@
1
1
  module Api::Auth::Helpers
2
+ # The 'memoize' macro makes sure only one query is issued to find the user
2
- def current_user? : User?
3
+ memoize def current_user? : User?
3
4
  auth_token.try do |value|
4
5
  user_from_auth_token(value)
5
6
  end
6
7
  end
7
8
 
8
9
  private def auth_token : String?
9
10
  bearer_token || token_param
10
11
  end
11
12
 
12
13
  private def bearer_token : String?
13
14
  context.request.headers["Authorization"]?
14
15
  .try(&.gsub("Bearer", ""))
15
16
  .try(&.strip)
16
17
  end
17
18
 
18
19
  private def token_param : String?
19
20
  params.get?(:auth_token)
20
21
  end
21
22
 
22
23
  private def user_from_auth_token(token : String) : User?
src/actions/mixins/api/auth/require_auth_token.cr CHANGED
@@ -12,23 +12,23 @@
12
12
  end
13
13
 
14
14
  private def auth_error_json
15
15
  ErrorSerializer.new(
16
16
  message: "Not authenticated.",
17
17
  details: auth_error_details
18
18
  )
19
19
  end
20
20
 
21
21
  private def auth_error_details : String
22
22
  if auth_token
23
23
  "The provided authentication token was incorrect."
24
24
  else
25
25
  "An authentication token is required. Please include a token in an 'auth_token' param or 'Authorization' header."
26
26
  end
27
27
  end
28
28
 
29
29
  # Tells the compiler that the current_user is not nil since we have checked
30
30
  # that the user is signed in
31
31
  private def current_user : User
32
- current_user?.not_nil!
32
+ current_user?.as(User)
33
33
  end
34
34
  end
src/actions/mixins/auth/require_sign_in.cr CHANGED
@@ -1,21 +1,21 @@
1
1
  module Auth::RequireSignIn
2
2
  macro included
3
3
  before require_sign_in
4
4
  end
5
5
 
6
6
  private def require_sign_in
7
7
  if current_user?
8
8
  continue
9
9
  else
10
10
  Authentic.remember_requested_path(self)
11
11
  flash.info = "Please sign in first"
12
12
  redirect to: SignIns::New
13
13
  end
14
14
  end
15
15
 
16
16
  # Tells the compiler that the current_user is not nil since we have checked
17
17
  # that the user is signed in
18
18
  private def current_user : User
19
- current_user?.not_nil!
19
+ current_user?.as(User)
20
20
  end
21
21
  end
src/components/shared/field.cr CHANGED
@@ -18,40 +18,40 @@
18
18
  #
19
19
  # You can customize this component so that fields render like you expect.
20
20
  # For example, you might wrap it in a div with a "field-wrapper" class.
21
21
  #
22
22
  # div class: "field-wrapper"
23
23
  # label_for field
24
24
  # yield field
25
25
  # mount Shared::FieldErrors, field
26
26
  # end
27
27
  #
28
28
  # You may also want to have more components if your fields look
29
29
  # different in different parts of your app, e.g. `CompactField` or
30
30
  # `InlineTextField`
31
31
  class Shared::Field(T) < BaseComponent
32
32
  # Raises a helpful error if component receives an unpermitted attribute
33
33
  include Lucky::CatchUnpermittedAttribute
34
34
 
35
35
  needs attribute : Avram::PermittedAttribute(T)
36
36
  needs label_text : String?
37
37
 
38
- def render
38
+ def render(&)
39
39
  label_for attribute, label_text
40
40
 
41
41
  # You can add more default options here. For example:
42
42
  #
43
43
  # tag_defaults field: attribute, class: "input"
44
44
  #
45
45
  # Will add the class "input" to the generated HTML.
46
46
  tag_defaults field: attribute do |tag_builder|
47
47
  yield tag_builder
48
48
  end
49
49
 
50
50
  mount Shared::FieldErrors, attribute
51
51
  end
52
52
 
53
53
  # Use a text_input by default
54
54
  def render
55
55
  render &.text_input
56
56
  end
57
57
  end
src/components/shared/layout_head.cr CHANGED
@@ -1,19 +1,18 @@
1
1
  class Shared::LayoutHead < BaseComponent
2
2
  needs page_title : String
3
3
 
4
4
  def render
5
5
  head do
6
6
  utf8_charset
7
7
  title "My App - #{@page_title}"
8
8
  css_link asset("css/app.css")
9
9
  js_link asset("js/app.js"), defer: "true"
10
10
  csrf_meta_tags
11
11
  responsive_meta_tag
12
12
 
13
- # Used only in development when running `lucky watch`.
13
+ # Development helper used with the `lucky watch` command.
14
- # Will reload browser whenever files change.
14
+ # Reloads the browser when files are updated.
15
- # See [docs]()
16
- live_reload_connect_tag
15
+ live_reload_connect_tag if LuckyEnv.development?
17
16
  end
18
17
  end
19
18
  end
src/models/user_token.cr CHANGED
@@ -5,26 +5,26 @@
5
5
 
6
6
  def self.generate(user : User) : String
7
7
  payload = {"user_id" => user.id}
8
8
 
9
9
  settings.stubbed_token || create_token(payload)
10
10
  end
11
11
 
12
12
  def self.create_token(payload)
13
13
  JWT.encode(payload, Lucky::Server.settings.secret_key_base, ALGORITHM)
14
14
  end
15
15
 
16
16
  def self.decode_user_id(token : String) : Int64?
17
17
  payload, _header = JWT.decode(token, Lucky::Server.settings.secret_key_base, ALGORITHM)
18
18
  payload["user_id"].to_s.to_i64
19
19
  rescue e : JWT::Error
20
20
  Lucky::Log.dexter.error { {jwt_decode_error: e.message} }
21
21
  nil
22
22
  end
23
23
 
24
24
  # Used in tests to return a fake token to test against.
25
- def self.stub_token(token : String)
25
+ def self.stub_token(token : String, &)
26
26
  temp_config(stubbed_token: token) do
27
27
  yield
28
28
  end
29
29
  end
30
30
  end