Elixir in Action 笔记 模块属性

2018/7/16 posted in  Elixir

Phoenix Channels Notes

可以实现数百万个长连接软实时通信

Channel 的概念很简单,客户端连接到服务器,订阅一个或者多个topic, 类似 public_chat updates:user1
发送给topic的任何消息(无论是从服务器还是从客户端发送)都会发送到订阅了该topic的所有客户端(包括发件人,如果订阅的话)

Overview

  • client可以使用websocket和long polling 连接服务端, 加入一个或者多个channel
  • 建立连接 客户端消息会被路由到不同的channel server
  • Channel 需要实现 join/3 terminate/2 handle_in/3 handle_out/3 回调函数

    endpoint 定义

    socket "/socket", HelloWeb.UserSocket,
    websocket: true,
    longpoll: false
2018/5/13 posted in  Elixir

Gitlab 维护笔记

备份

gitlab-rake gitlab:backup:create STRATEGY=copy

备份位置

/var/opt/gitlab/backups

升级

apt-get update && sudo apt-get install gitlab-ce
2018/4/20 posted in  DevOps

Phoenix 使用纪录 集成 Swagger

使用这个项目 xerions/phoenix_swagger: Swagger integration to Phoenix framework

定义schema

示例

def swagger_definitions do
  %{
    User: swagger_schema do
      title "User"
      description "A user of the application"
      properties do
        name :string, "Users name", required: true
        id :string, "Unique identifier", required: true
        address :string, "Home address"
        preferences (Schema.new do
          properties do
            subscribe_to_mailing_list :boolean, "mailing list subscription", default: true
            send_special_offers :boolean, "special offers list subscription", default: true
          end
        end)
      end
      example %{
        name: "Joe",
        id: "123",
        address: "742 Evergreen Terrace"
      }
    end,
    Users: swagger_schema do
      title "Users"
      description "A collection of Users"
      type :array
      items Schema.ref(:User)
    end
  }
end

定义action参数

PhoenixSwagger 提供了swagger_path/2宏,它为某些phoenix控制器action生成swagger规范

use PhoenixSwagger

swagger_path :index do
  get "/posts"
  description "List blog posts"
  response 200, "Success"
end

def index(conn, _params) do
  posts = Repo.all(Post)
  render(conn, "index.json", posts: posts)
end

如何在 header 中的 api-key 认证

定义在Router模块中的 swagger_info 函数
配置 securityDefinitionssecurity 属性

 def swagger_info do
    %{
      :swagger => "2.0",
      :info => %{
        version: "0.0.1",
        title: "My API",
        description: "My Awesome API"
      },
      consumes: [
        "application/json"
      ],
      produces: [
        "application/json"
      ],
      definitions: %{},
      paths: %{},
      securityDefinitions: %{
        MyApiKey: %{
          type: "apiKey",
          name: "Authorization",
          in: "header",
          description: "API Operations require a valid API Key."
        }
      },
      security: [
        %{MyApiKey: []}
      ],
    }
  end
  

这个所有action都会用到

也可以指定单个actoin参数, 下面parameters 中的 :header

  swagger_path :show do
    get "/api/users/{id}"
    summary "Show user"
    description "Show a user by ID"

    parameters do
      id :path, :string, "user ID", required: true
      token :header, :string, "token to be passed as a header", required: true
    end

    response 200, "OK", Schema.ref(:User)
    response 401, "Unauthorized"
  end

links

2018/4/19 posted in  Elixir

Phoenix 使用纪录 自定义json返回错误

lib/my_app_web/views/error_view.ex

  def render("404.json", _assigns) do
    %{errors: %{detail: "Endpoint not found!"}}
  end

  def render("401.json", _assigns) do
    %{errors: %{detail: "Unauthorized!"}}
  end

  def render("500.json", _assigns) do
    %{errors: %{detail: "Internal server error :("}}
  end

使用 Phoenix.Controller.render/3 渲染模板

conn |> put_status(:unauthorized) |> halt() |> render(CenterWeb.ErrorView, "401.json")
2018/4/18 posted in  Elixir

Phoenix 使用纪录 函数plug 和 模块plug

Phoenix的核心组件 Endpoints, Routers, Controllers都是 plug

plug 分为两类 Function plug 和 Module plug

Function Plugs

函数接收 %Plug.Conn{} 参数 和 Options 参数, 并返回 conn, 这样的函数就可以当作plug

def put_headers(conn, key_values) do
  Enum.reduce key_values, conn, fn {k, v}, conn ->
    Plug.Conn.put_resp_header(conn, to_string(k), v)
  end
end

在phoenix框架里使用

defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  plug :put_headers, %{content_encoding: "gzip", cache_control: "max-age=3600"}
  plug :put_layout, "bare.html"

  ...
end

另外一个例子,比如我们会在controller做一系列检查,当失败时中止请求或者重定向,如果不用plug,大概实现是这样


defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  def show(conn, params) do
    case authenticate(conn) do
      {:ok, user} ->
        case find_message(params["id"]) do
          nil ->
            conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/")
          message ->
            case authorize_message(conn, params["id"]) do
              :ok ->
                render conn, :show, page: find_message(params["id"])
              :error ->
                conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/")
            end
        end
      :error ->
        conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/")
    end
  end
end

代码中各种嵌套, 非常不易阅读

使用plug重构

defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  plug :authenticate
  plug :fetch_message
  plug :authorize_message

  def show(conn, params) do
    render conn, :show, page: find_message(params["id"])
  end

  defp authenticate(conn, _) do
    case Authenticator.find_user(conn) do
      {:ok, user} ->
        assign(conn, :user, user)
      :error ->
        conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/") |> halt()
    end
  end

  defp fetch_message(conn, _) do
    case find_message(conn.params["id"]) do
      nil ->
        conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/") |> halt()
      message ->
        assign(conn, :message, message)
    end
  end

  defp authorize_message(conn, _) do
    if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do
      conn
    else
      conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/") |> halt()
    end
  end
end

这样代码更容易维护,以及重用

Module Plugs

基本模块的plug, 需要实现两个函数

  • init/1 初始化,返回值作为call/2的参数
  • call/2 类似前面看到的函数plug

    示例,实现一个module plug, 处理:locale 参数, 设置assigns值, 可以让后边要处理的plug使用


defmodule HelloWeb.Plugs.Locale do
  import Plug.Conn

  @locales ["en", "fr", "de"]

  def init(default), do: default

  def call(%Plug.Conn{params: %{"locale" => loc}} = conn, _default) when loc in @locales do
    assign(conn, :locale, loc)
  end
  def call(conn, default),  do: assign(conn, :locale, default)
end

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug HelloWeb.Plugs.Locale, "en"
  end
  ...
  

links

2018/4/18 posted in  Elixir

Phoenix 使用纪录 Action Fallback

宏实现

 defmacro action_fallback(plug) do
    Phoenix.Controller.Pipeline.__action_fallback__(plug)
 end

将一个plug注册到 pipeline里, 用来在controller出现错误时,统一进行错误处理

如下 show action 代码 else 分支会对错误进行处理, 可能的两种错误情况 {:error, :not_found}
{:error, :unauthorized}

 defmodule HelloWeb.MyController do
  use Phoenix.Controller
  alias Hello.{Authorizer, Blog}
  alias HelloWeb.ErrorView

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- Blog.fetch_post(id),
         :ok <- Authorizer.authorize(current_user, :view, post) do

      render(conn, "show.json", post: post)
    else
      {:error, :not_found} ->
        conn
        |> put_status(:not_found)
        |> put_view(ErrorView)
        |> render(:"404")
      {:error, :unauthorized} ->
        conn
        |> put_status(403)
        |> put_view(ErrorView)
        |> render(:"403")
    end
  end
end

在实现API的controllerd时 controller中的错误处理会导致很多重复

实现一个plug统一错误处理

defmodule HelloWeb.MyFallbackController do
  use Phoenix.Controller
  alias HelloWeb.ErrorView

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(ErrorView)
    |> render(:"404")
  end

  def call(conn, {:error, :unauthorized}) do
    conn
    |> put_status(403)
    |> put_view(ErrorView)
    |> render(:"403")
  end
end

定义action_fallback 统一处理错误,可以让controller非常简单清晰

defmodule HelloWeb.MyController do
  use Phoenix.Controller
  alias Hello.{Authorizer, Blog}

  action_fallback HelloWeb.MyFallbackController

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- Blog.fetch_post(id),
         :ok <- Authorizer.authorize(current_user, :view, post) do

      render(conn, "show.json", post: post)
    end
  end
end

links

2018/4/18 posted in  Elixir

Phoenix 使用纪录 gen tasks

2018/4/18 posted in  Elixir

Phoenix 使用纪录 Gitlab OAuth2 认证笔记

需要在gitlab后台创建application

在mix.exs 添加依赖

{:ueberauth, "~> 0.4"},
{:ueberauth_gitlab, "~> 0.1"}

项目主页

extra_applications 配置

-      extra_applications: [:logger, :runtime_tools]
+      extra_applications: [:logger, :runtime_tools, :ueberauth, :ueberauth_gitlab]

config.exs 配置

config :ueberauth, Ueberauth,
  providers: [
    gitlab: {Ueberauth.Strategy.Gitlab, []}
  ]

config :ueberauth, Ueberauth.Strategy.Gitlab.OAuth,
  client_id: System.get_env("GITLAB_CLIENT_ID"),
  client_secret: System.get_env("GITLAB_CLIENT_SECRET")
  site: "https://git.3303033.com/",
  authorize_url: "https://git.3303033.com/oauth/authorize",
  token_url: "https://git.3303033.com/oauth/token"

site authorize_url token_url 配置成自己搭建的gitlab服务器

配置路由

  require Ueberauth
  scope "/auth", UeberauthGitlabDemoWeb do
    pipe_through [:browser]

    get "/:provider", AuthController, :request
    get "/:provider/callback", AuthController, :callback
    post "/:provider/callback", AuthController, :callback
    delete "/logout", AuthController, :delete
  end

认证地址 http://localhost:4000/auth/gitlab

成功返回会重定向到 /auth/:provider/callback,结果大概这个样子

{
   "uid":"lidashuang",
   "strategy":"Elixir.Ueberauth.Strategy.Gitlab",
   "provider":"gitlab",
   "info":{
      "urls":{
         "website_url":"",
         "web_url":"https://git.3303033.com/lidashuang",
         "avatar_url":"https://git.3303033.com/uploads/-/system/user/avatar/3/avatar.png"
      },
   
   },
   "extra":{
      
   }
}

详细
https://gist.github.com/zw0/9624f41952823cc7894650ea833c4a4d

auth_controller.ex 处理callback

defmodule UeberauthGitlabDemoWeb.AuthController do
  use UeberauthGitlabDemoWeb, :controller
  plug Ueberauth

  def delete(conn, _params) do
    conn
    |> put_flash(:info, "You have been logged out!")
    |> configure_session(drop: true)
    |> redirect(to: "/")
  end

  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: "/")
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    conn
    |> put_flash(:info, "Successfully authenticated.")
    |> put_session(:uid, auth.uid)
    |> redirect(to: "/")
  end
end

所有代码 https://gitee.com/ifsclimbing/ueberauth_gitlab_demo

links

2018/4/11 posted in  Elixir

Phoenix.Endpoint configuration

docs: https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#module-runtime-configuration

配置主要有两类 Compile-time configurationRuntime configuration

Compile-time configuration 表示只能编译时读取,运行时不能修改

Runtime configuration运行时配置,是应用启动后才可以读取的,使用config/2 函数

YourApp.Endpoint.config(:port)
YourApp.Endpoint.config(:some_config, :default_value)

Compile-time configuration

  • :code_reloader 是否启用代码重新加载功能
  • :debug_errors 设置为true,则使用 Plug.Debugger 处理应用失败,建议仅在开发时将其设置为true,因为它允许在调试期间列出应用程序源代码
  • :render_errors 应用出错时,渲染模板配置,比如在HTML request,出现500错误时 render("500.html", assigns)渲染模板处理 默认配置是[view: MyApp.ErrorView, accepts: ~w(html), layout: false]

Runtime configuration

  • :http HTTP server 配置,现在默认使用Cowboy,Cowboy选项 Plug.Adapters.Cowboy
  • :https HTTPS server配置,Cowboy选项 Plug.Adapters.Cowboy
  • :force_ssl 强制使用https,Plug.SSL,默认设置strict-transport-security header,强制浏览器使用https
  • :url 用于在整个应用程序中生成URL的配置,有 host, :scheme, :path, :port 的配置

https 配置示例

certbot ssl配置 https://certbot.eff.org

config :books, BooksWeb.Endpoint,
  load_from_system_env: false,
  url: [host: "example.com", port: 443],
  # http: [port: 80],
  force_ssl: [hsts: true],
  https: [:inet6,
          port: 443,
          keyfile: "/etc/letsencrypt/live/example.com/privkey.pem",
          cacertfile: "/etc/letsencrypt/live/example.com/chain.pem",
          certfile: "/etc/letsencrypt/live/example.com/cert.pem"]

配置 force_ssl: [hsts: true] 强制https

配置 http: [port: 80] 同时支持 http & https

2018/4/4 posted in  Elixir