@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.7.3
|
@@ -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.
|
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.
|
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
|
@@ -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).
|
@@ -1,53 +1,46 @@
|
|
1
1
|
|
2
2
|
|
3
3
|
set -euo pipefail
|
4
4
|
|
5
|
-
# This is the entrypoint script used for development docker workflows.
|
5
|
+
# This is the entrypoint script used for development docker workflows.
|
6
|
-
# default it will:
|
6
|
+
# By default it will:
|
7
|
-
# - Install
|
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
|
@@ -1,40 +1,34 @@
|
|
1
|
-
FROM crystallang/crystal: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
|
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
|
23
|
+
# Install lucky cli
|
31
24
|
WORKDIR /lucky/cli
|
32
25
|
RUN git clone https://github.com/luckyframework/lucky_cli . && \
|
33
|
-
git checkout
|
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
|
33
|
+
EXPOSE 3001
|
40
34
|
|
@@ -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
|
-
-
|
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
|
|
@@ -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.
|
11
|
+
crystal: 1.7.3
|
12
12
|
|
13
13
|
dependencies:
|
14
14
|
lucky:
|
15
15
|
github: luckyframework/lucky
|
16
|
-
version: ~> 1.0.0
|
16
|
+
version: ~> 1.0.0
|
17
17
|
avram:
|
18
18
|
github: luckyframework/avram
|
19
|
-
version: ~> 1.0.0
|
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.
|
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
|
@@ -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
|
@@ -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
|
@@ -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?
|
@@ -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?.
|
32
|
+
current_user?.as(User)
|
33
33
|
end
|
34
34
|
end
|
@@ -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?.
|
19
|
+
current_user?.as(User)
|
20
20
|
end
|
21
21
|
end
|
@@ -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
|
@@ -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
|
-
#
|
13
|
+
# Development helper used with the `lucky watch` command.
|
14
|
-
#
|
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
|
@@ -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
|