@@ -1 +1 @@
|
|
1
|
-
1.1
|
1
|
+
1.2.1
|
@@ -1,75 +1,81 @@
|
|
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
|
+
strategy:
|
12
|
+
fail-fast: false
|
13
|
+
matrix:
|
14
|
+
crystal_version:
|
15
|
+
- 1.2.1
|
16
|
+
experimental:
|
17
|
+
- false
|
11
18
|
runs-on: ubuntu-latest
|
12
|
-
container:
|
13
|
-
|
19
|
+
continue-on-error: ${{ matrix.experimental }}
|
14
20
|
steps:
|
15
21
|
- uses: actions/checkout@v2
|
22
|
+
- name: Install Crystal
|
23
|
+
uses: crystal-lang/install-crystal@v1
|
24
|
+
with:
|
25
|
+
crystal: ${{ matrix.crystal_version }}
|
16
26
|
- name: Format
|
17
27
|
run: crystal tool format --check
|
18
28
|
|
19
29
|
specs:
|
30
|
+
strategy:
|
31
|
+
fail-fast: false
|
32
|
+
matrix:
|
33
|
+
crystal_version:
|
34
|
+
- 1.2.1
|
35
|
+
experimental:
|
36
|
+
- false
|
20
37
|
runs-on: ubuntu-latest
|
21
|
-
container:
|
22
|
-
image: crystallang/crystal:1.1.0
|
23
38
|
env:
|
24
39
|
LUCKY_ENV: test
|
25
40
|
DB_HOST: postgres
|
26
|
-
|
41
|
+
continue-on-error: ${{ matrix.experimental }}
|
27
42
|
services:
|
28
43
|
postgres:
|
29
44
|
image: postgres:12-alpine
|
30
45
|
env:
|
31
46
|
POSTGRES_PASSWORD: postgres
|
32
47
|
ports:
|
33
48
|
- 5432:5432
|
34
49
|
# Set health checks to wait until postgres has started
|
35
50
|
options: >-
|
36
51
|
--health-cmd pg_isready
|
37
52
|
--health-interval 10s
|
38
53
|
--health-timeout 5s
|
39
54
|
--health-retries 5
|
40
55
|
|
41
56
|
steps:
|
42
57
|
- uses: actions/checkout@v2
|
43
|
-
|
44
|
-
- name: Install PostgreSQL client
|
45
|
-
run: |
|
46
|
-
apt-get update
|
47
|
-
apt-get -yqq install libpq-dev postgresql-client
|
48
|
-
- name: Install
|
58
|
+
- name: Install Crystal
|
49
|
-
run: apt-get -yqq install chromium-browser
|
50
|
-
|
51
|
-
|
59
|
+
uses: crystal-lang/install-crystal@v1
|
52
60
|
with:
|
53
|
-
node-version: '12.x'
|
54
|
-
|
61
|
+
crystal: ${{ matrix.crystal_version }}
|
55
|
-
run: npm install -g yarn
|
56
62
|
|
57
63
|
- name: Get yarn cache directory path
|
58
64
|
id: yarn-cache-dir-path
|
59
65
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
60
66
|
|
61
67
|
- name: Set up Yarn cache
|
62
68
|
uses: actions/cache@v2
|
63
69
|
with:
|
64
70
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
65
71
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
66
72
|
restore-keys: |
|
67
73
|
${{ runner.os }}-yarn-
|
68
74
|
|
69
75
|
- name: Set up Node cache
|
70
76
|
uses: actions/cache@v2
|
71
77
|
id: node-cache # use this to check for `cache-hit` (`steps.node-cache.outputs.cache-hit != 'true'`)
|
72
78
|
with:
|
73
79
|
path: '**/node_modules'
|
74
80
|
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
75
81
|
restore-keys: |
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# This file may be used for custom Application configurations.
|
2
|
+
# It will be loaded before other config files.
|
3
|
+
#
|
4
|
+
# Read more on configuration:
|
5
|
+
# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code
|
6
|
+
|
7
|
+
# Use this code as an example:
|
8
|
+
#
|
9
|
+
# ```
|
10
|
+
# module Application
|
11
|
+
# Habitat.create do
|
12
|
+
# setting support_email : String
|
13
|
+
# setting lock_with_basic_auth : Bool
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Application.configure do |settings|
|
18
|
+
# settings.support_email = "support@myapp.io"
|
19
|
+
# settings.lock_with_basic_auth = LuckEnv.staging?
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # In your application, call
|
23
|
+
# # `Application.settings.support_email` anywhere you need it.
|
24
|
+
# ```
|
@@ -1,10 +1,11 @@
|
|
1
1
|
require "./server"
|
2
2
|
|
3
3
|
Authentic.configure do |settings|
|
4
4
|
settings.secret_key = Lucky::Server.settings.secret_key_base
|
5
5
|
|
6
6
|
unless LuckyEnv.production?
|
7
|
+
# This value can be between 4 and 31
|
7
8
|
fastest_encryption_possible = 4
|
8
9
|
settings.encryption_cost = fastest_encryption_possible
|
9
10
|
end
|
10
11
|
end
|
@@ -29,28 +29,38 @@
|
|
29
29
|
# if LuckyEnv.production?
|
30
30
|
# settings.asset_host = "https://mycdnhost.com"
|
31
31
|
# else
|
32
32
|
# settings.asset_host = ""
|
33
33
|
# end
|
34
34
|
# end
|
35
35
|
settings.asset_host = "" # Lucky will serve assets
|
36
36
|
end
|
37
37
|
|
38
38
|
Lucky::ForceSSLHandler.configure do |settings|
|
39
39
|
# To force SSL in production, uncomment the lines below.
|
40
40
|
# This will cause http requests to be redirected to https:
|
41
41
|
#
|
42
42
|
# settings.enabled = LuckyEnv.production?
|
43
43
|
# settings.strict_transport_security = {max_age: 1.year, include_subdomains: true}
|
44
44
|
#
|
45
45
|
# Or, leave it disabled:
|
46
46
|
settings.enabled = false
|
47
47
|
end
|
48
48
|
|
49
|
+
# Set a uniuqe ID for each HTTP request.
|
50
|
+
Lucky::RequestIdHandler.configure do |settings|
|
51
|
+
# To enable the request ID, uncomment the lines below.
|
52
|
+
# You can set your own custom String, or use a random UUID.
|
53
|
+
#
|
54
|
+
# settings.set_request_id = ->(context : HTTP::Server::Context) {
|
55
|
+
# UUID.random.to_s
|
56
|
+
# }
|
57
|
+
end
|
58
|
+
|
49
59
|
private def secret_key_from_env
|
50
60
|
ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production
|
51
61
|
end
|
52
62
|
|
53
63
|
private def raise_missing_secret_key_in_production
|
54
64
|
puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red
|
55
65
|
exit(1)
|
56
66
|
end
|
@@ -1,25 +1,25 @@
|
|
1
1
|
{
|
2
2
|
"license": "UNLICENSED",
|
3
3
|
"private": true,
|
4
4
|
"dependencies": {
|
5
5
|
"@rails/ujs": "^6.0.0",
|
6
6
|
"modern-normalize": "^1.1.0",
|
7
7
|
"turbolinks": "^5.2.0"
|
8
8
|
},
|
9
9
|
"scripts": {
|
10
10
|
"heroku-postbuild": "yarn prod",
|
11
|
-
"dev": "mix",
|
11
|
+
"dev": "yarn run mix",
|
12
|
-
"watch": "mix watch",
|
12
|
+
"watch": "yarn run mix watch",
|
13
|
-
"prod": "mix --production"
|
13
|
+
"prod": "yarn run mix --production"
|
14
14
|
},
|
15
15
|
"devDependencies": {
|
16
16
|
"@babel/compat-data": "^7.9.0",
|
17
17
|
"browser-sync": "^2.26.7",
|
18
18
|
"compression-webpack-plugin": "^7.0.0",
|
19
19
|
"laravel-mix": "^6.0.0",
|
20
20
|
"postcss": "^8.1.0",
|
21
21
|
"resolve-url-loader": "^3.1.1",
|
22
22
|
"sass": "^1.26.10",
|
23
23
|
"sass-loader": "^10.0.2"
|
24
24
|
}
|
25
25
|
}
|
@@ -0,0 +1,4 @@
|
|
1
|
+
{
|
2
|
+
"/css/app.css": "/css/app.css",
|
3
|
+
"/js/app.js": "/js/app.js"
|
4
|
+
}
|
@@ -1,38 +1,38 @@
|
|
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
|
my_app:
|
9
9
|
main: src/my_app.cr
|
10
10
|
|
11
|
-
crystal: 1.1
|
11
|
+
crystal: 1.2.1
|
12
12
|
|
13
13
|
dependencies:
|
14
14
|
lucky:
|
15
15
|
github: luckyframework/lucky
|
16
|
-
version: ~> 0.
|
16
|
+
version: ~> 0.29.0
|
17
17
|
authentic:
|
18
18
|
github: luckyframework/authentic
|
19
|
-
version: ~> 0.8.
|
19
|
+
version: ~> 0.8.1
|
20
20
|
carbon:
|
21
21
|
github: luckyframework/carbon
|
22
22
|
version: ~> 0.2.0
|
23
23
|
carbon_sendgrid_adapter:
|
24
24
|
github: luckyframework/carbon_sendgrid_adapter
|
25
|
-
version: ~> 0.
|
25
|
+
version: ~> 0.2.0
|
26
26
|
lucky_env:
|
27
27
|
github: luckyframework/lucky_env
|
28
|
-
version: ~> 0.1.
|
28
|
+
version: ~> 0.1.4
|
29
29
|
lucky_task:
|
30
30
|
github: luckyframework/lucky_task
|
31
|
-
version: ~> 0.1.
|
31
|
+
version: ~> 0.1.1
|
32
32
|
jwt:
|
33
33
|
github: crystal-community/jwt
|
34
|
-
version: ~> 1.
|
34
|
+
version: ~> 1.6.0
|
35
35
|
development_dependencies:
|
36
36
|
lucky_flow:
|
37
37
|
github: luckyframework/lucky_flow
|
38
38
|
version: ~> 0.7.3
|
@@ -1,23 +1,23 @@
|
|
1
1
|
require "../spec_helper"
|
2
2
|
|
3
|
-
describe "Authentication flow" do
|
3
|
+
describe "Authentication flow", tags: "flow" do
|
4
4
|
it "works" do
|
5
5
|
flow = AuthenticationFlow.new("test@example.com")
|
6
6
|
|
7
7
|
flow.sign_up "password"
|
8
8
|
flow.should_be_signed_in
|
9
9
|
flow.sign_out
|
10
10
|
flow.sign_in "wrong-password"
|
11
11
|
flow.should_have_password_error
|
12
12
|
flow.sign_in "password"
|
13
13
|
flow.should_be_signed_in
|
14
14
|
end
|
15
15
|
|
16
16
|
# This is to show you how to sign in as a user during tests.
|
17
17
|
# Use the `visit` method's `as` option in your tests to sign in as that user.
|
18
18
|
#
|
19
19
|
# Feel free to delete this once you have other tests using the 'as' option.
|
20
20
|
it "allows sign in through backdoor when testing" do
|
21
21
|
user = UserFactory.create
|
22
22
|
flow = BaseFlow.new
|
23
23
|
|
@@ -1,18 +1,18 @@
|
|
1
1
|
require "../spec_helper"
|
2
2
|
|
3
|
-
describe "Reset password flow" do
|
3
|
+
describe "Reset password flow", tags: "flow" do
|
4
4
|
it "works" do
|
5
5
|
user = UserFactory.create
|
6
6
|
flow = ResetPasswordFlow.new(user)
|
7
7
|
|
8
8
|
flow.request_password_reset
|
9
9
|
flow.should_have_sent_reset_email
|
10
10
|
flow.reset_password "new-password"
|
11
11
|
flow.should_be_signed_in
|
12
12
|
flow.sign_out
|
13
13
|
flow.sign_in "wrong-password"
|
14
14
|
flow.should_have_password_error
|
15
15
|
flow.sign_in "new-password"
|
16
16
|
flow.should_be_signed_in
|
17
17
|
end
|
18
18
|
end
|
@@ -1,17 +1,17 @@
|
|
1
1
|
# Include modules and add methods that are for all API requests
|
2
2
|
abstract class ApiAction < Lucky::Action
|
3
3
|
# APIs typically do not need to send cookie/session data.
|
4
4
|
# Remove this line if you want to send cookies in the response header.
|
5
5
|
disable_cookies
|
6
6
|
accepted_formats [:json]
|
7
7
|
|
8
8
|
include Api::Auth::Helpers
|
9
9
|
|
10
10
|
# By default all actions require sign in.
|
11
11
|
# Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests.
|
12
12
|
include Api::Auth::RequireAuthToken
|
13
13
|
|
14
|
-
# By default all actions are required to use underscores.
|
14
|
+
# By default all actions are required to use underscores to separate words.
|
15
15
|
# Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes.
|
16
16
|
include Lucky::EnforceUnderscoredRoute
|
17
17
|
end
|
@@ -1,22 +1,25 @@
|
|
1
1
|
abstract class BrowserAction < Lucky::Action
|
2
2
|
include Lucky::ProtectFromForgery
|
3
|
+
|
4
|
+
# By default all actions are required to use underscores.
|
5
|
+
# Add `include Lucky::SkipRouteStyleCheck` to your actions if you wish to ignore this check for specific routes.
|
3
6
|
include Lucky::EnforceUnderscoredRoute
|
4
7
|
|
5
8
|
# This module disables Google FLoC by setting the
|
6
9
|
# [Permissions-Policy](https://github.com/WICG/floc) HTTP header to `interest-cohort=()`.
|
7
10
|
#
|
8
11
|
# This header is a part of Google's Federated Learning of Cohorts (FLoC) which is used
|
9
12
|
# to track browsing history instead of using 3rd-party cookies.
|
10
13
|
#
|
11
14
|
# Remove this include if you want to use the FLoC tracking.
|
12
15
|
include Lucky::SecureHeaders::DisableFLoC
|
13
16
|
|
14
17
|
accepted_formats [:html, :json], default: :html
|
15
18
|
|
16
19
|
# This module provides current_user, sign_in, and sign_out methods
|
17
20
|
include Authentic::ActionHelpers(User)
|
18
21
|
|
19
22
|
# When testing you can skip normal sign in by using `visit` with the `as` param
|
20
23
|
#
|
21
24
|
# flow.visit Me::Show, as: UserFactory.create
|
22
25
|
include Auth::TestBackdoor
|
@@ -33,31 +33,31 @@
|
|
33
33
|
def render(error : Lucky::RenderableError)
|
34
34
|
if html?
|
35
35
|
error_html DEFAULT_MESSAGE, status: error.renderable_status
|
36
36
|
else
|
37
37
|
error_json error.renderable_message, status: error.renderable_status
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
# If none of the 'render' methods return a response for the raised Exception,
|
42
42
|
# Lucky will use this method.
|
43
43
|
def default_render(error : Exception) : Lucky::Response
|
44
44
|
if html?
|
45
45
|
error_html DEFAULT_MESSAGE, status: 500
|
46
46
|
else
|
47
47
|
error_json DEFAULT_MESSAGE, status: 500
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
51
|
private def error_html(message : String, status : Int)
|
52
52
|
context.response.status_code = status
|
53
|
-
|
53
|
+
html_with_status Errors::ShowPage, status, message: message, status_code: status
|
54
54
|
end
|
55
55
|
|
56
56
|
private def error_json(message : String, status : Int, details = nil, param = nil)
|
57
57
|
json ErrorSerializer.new(message: message, details: details, param: param), status: status
|
58
58
|
end
|
59
59
|
|
60
60
|
private def report(error : Exception) : Nil
|
61
61
|
# Send to Rollbar, send an email, etc.
|
62
62
|
end
|
63
63
|
end
|
@@ -1,26 +1,26 @@
|
|
1
1
|
require "./shards"
|
2
2
|
|
3
|
+
# Load the asset manifest
|
3
|
-
|
4
|
+
Lucky::AssetHelpers.load_manifest "public/mix-manifest.json"
|
4
|
-
Lucky::AssetHelpers.load_manifest
|
5
5
|
|
6
|
+
require "../config/server"
|
7
|
+
require "../config/**"
|
6
8
|
require "./app_database"
|
7
9
|
require "./models/base_model"
|
8
10
|
require "./models/mixins/**"
|
9
11
|
require "./models/**"
|
10
12
|
require "./queries/mixins/**"
|
11
13
|
require "./queries/**"
|
12
14
|
require "./operations/mixins/**"
|
13
15
|
require "./operations/**"
|
14
16
|
require "./serializers/base_serializer"
|
15
17
|
require "./serializers/**"
|
16
18
|
require "./emails/base_email"
|
17
19
|
require "./emails/**"
|
18
20
|
require "./actions/mixins/**"
|
19
21
|
require "./actions/**"
|
20
22
|
require "./components/base_component"
|
21
23
|
require "./components/**"
|
22
24
|
require "./pages/**"
|
23
|
-
require "../config/server"
|
24
|
-
require "../config/**"
|
25
25
|
require "../db/migrations/**"
|
26
26
|
require "./app_server"
|
@@ -1,27 +1,26 @@
|
|
1
1
|
class AppServer < Lucky::BaseAppServer
|
2
2
|
# Learn about middleware with HTTP::Handlers:
|
3
3
|
# https://luckyframework.org/guides/http-and-routing/http-handlers
|
4
4
|
def middleware : Array(HTTP::Handler)
|
5
5
|
[
|
6
|
+
Lucky::RequestIdHandler.new,
|
6
7
|
Lucky::ForceSSLHandler.new,
|
7
8
|
Lucky::HttpMethodOverrideHandler.new,
|
8
9
|
Lucky::LogHandler.new,
|
9
10
|
Lucky::ErrorHandler.new(action: Errors::Show),
|
10
11
|
Lucky::RemoteIpHandler.new,
|
11
12
|
Lucky::RouteHandler.new,
|
12
13
|
Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"),
|
13
14
|
Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false),
|
14
15
|
Lucky::RouteNotFoundHandler.new,
|
15
16
|
] of HTTP::Handler
|
16
17
|
end
|
17
18
|
|
18
19
|
def protocol
|
19
20
|
"http"
|
20
21
|
end
|
21
22
|
|
22
23
|
def listen
|
23
|
-
# Learn about bind_tcp: https://tinyurl.com/bind-tcp-docs
|
24
|
-
server.
|
24
|
+
server.listen(host, port, reuse_port: false)
|
25
|
-
server.listen
|
26
25
|
end
|
27
26
|
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module PasswordValidations
|
2
2
|
macro included
|
3
3
|
before_save run_password_validations
|
4
4
|
end
|
5
5
|
|
6
6
|
private def run_password_validations
|
7
7
|
validate_required password, password_confirmation
|
8
8
|
validate_confirmation_of password, with: password_confirmation
|
9
|
+
# 72 is a limitation of BCrypt
|
9
|
-
validate_size_of password, min: 6
|
10
|
+
validate_size_of password, min: 6, max: 72
|
10
11
|
end
|
11
12
|
end
|
@@ -1,40 +1,40 @@
|
|
1
1
|
class Errors::ShowPage
|
2
2
|
include Lucky::HTMLPage
|
3
3
|
|
4
4
|
needs message : String
|
5
|
-
needs
|
5
|
+
needs status_code : Int32
|
6
6
|
|
7
7
|
def render
|
8
8
|
html_doctype
|
9
9
|
html lang: "en" do
|
10
10
|
head do
|
11
11
|
utf8_charset
|
12
12
|
title "Something went wrong"
|
13
13
|
load_lato_font
|
14
14
|
normalize_styles
|
15
15
|
error_page_styles
|
16
16
|
end
|
17
17
|
|
18
18
|
body do
|
19
19
|
div class: "container" do
|
20
|
-
h2
|
20
|
+
h2 status_code, class: "status-code"
|
21
21
|
h1 message, class: "message"
|
22
22
|
|
23
23
|
ul class: "helpful-links" do
|
24
24
|
li do
|
25
25
|
a "Try heading back to home", href: "/", class: "helpful-link"
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
33
|
def load_lato_font
|
34
34
|
css_link "https://fonts.googleapis.com/css?family=Lato"
|
35
35
|
end
|
36
36
|
|
37
37
|
def normalize_styles
|
38
38
|
style <<-CSS
|
39
39
|
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}
|
40
40
|
CSS
|