├── assets ├── test │ ├── __mocks__ │ │ ├── styleMock.js │ │ └── fileMock.js │ └── App.test.js ├── .babelrc ├── static │ ├── images │ │ ├── openpgp.worker.min.js │ │ ├── matt.jpg │ │ └── phoenix.png │ ├── favicon.ico │ └── robots.txt ├── js │ ├── utils │ │ └── api.js │ ├── socket.js │ ├── actions │ │ ├── usersAction.js │ │ ├── messageActions.js │ │ ├── userActions.js │ │ └── cryptoActions.js │ ├── components │ │ ├── TagsDropdown.js │ │ ├── InviteUserModal.js │ │ ├── ColorPicker.js │ │ ├── Root.js │ │ ├── RenderedUrl.js │ │ ├── ExportKey.js │ │ ├── MessageForm.js │ │ ├── OnlineUsersDropdown.js │ │ ├── SignUp.js │ │ ├── RenderedMessage.js │ │ ├── UserModal.js │ │ ├── ChatSegment.js │ │ ├── MainMenuDropdown.js │ │ ├── Nav.js │ │ ├── Home.js │ │ ├── HomepageLayout.js │ │ └── Main.js │ ├── reducers │ │ ├── usersReducer.js │ │ ├── appReducer.js │ │ ├── index.js │ │ ├── cryptoReducer.js │ │ ├── userReducer.js │ │ └── messageReducer.js │ ├── app.js │ └── registerServiceWorker.js ├── theme │ ├── site │ │ └── globals │ │ │ └── site.overrides │ ├── theme.config │ └── semantic.less ├── css │ └── app.css ├── package.json ├── webpack.config.js └── webpack.config.prod.js ├── .dockerignore ├── scripts ├── attach.sh ├── run_dev.sh ├── run_prod.sh └── run_gcp.sh ├── datamodel.mwb ├── react-logo.png ├── lib ├── app_web │ ├── views │ │ ├── page_view.ex │ │ ├── layout_view.ex │ │ ├── team_view.ex │ │ ├── tag_view.ex │ │ ├── user_request_view.ex │ │ ├── user_view.ex │ │ ├── error_view.ex │ │ ├── request_view.ex │ │ ├── message_view.ex │ │ └── error_helpers.ex │ ├── controllers │ │ ├── page_controller.ex │ │ ├── team_controller.ex │ │ └── user_controller.ex │ ├── templates │ │ ├── layout │ │ │ └── app.html.eex │ │ └── page │ │ │ └── index.html.eex │ ├── gettext.ex │ ├── router.ex │ ├── channels │ │ ├── user_socket.ex │ │ ├── presence.ex │ │ ├── user_channel.ex │ │ └── room_channel.ex │ └── endpoint.ex ├── app │ ├── user_manager │ │ ├── error_handler.ex │ │ ├── guardian.ex │ │ ├── pipeline.ex │ │ └── user_manager.ex │ ├── repo.ex │ ├── message_tag.ex │ ├── team.ex │ ├── user_request.ex │ ├── tag.ex │ ├── message.ex │ ├── request.ex │ ├── application.ex │ └── user.ex ├── app.ex └── app_web.ex ├── test ├── test_helper.exs ├── app_web │ ├── views │ │ ├── page_view_test.exs │ │ └── layout_view_test.exs │ └── channels │ │ └── room_channel_test.exs └── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── data_case.ex ├── docker-compose.prod.yml ├── docker-entrypoint.sh ├── priv ├── repo │ ├── migrations │ │ ├── 20180428193155_add_body_index_to_tags.exs │ │ ├── 20180428200542_add_name_index_to_teams.exs │ │ ├── 20180530191317_add_unique_constraint_to_tags.exs │ │ ├── 20180507182458_add_color_to_user.exs │ │ ├── 20180606185126_add_avatar_to_users.exs │ │ ├── 20180609181732_add_avatar_to_requests.exs │ │ ├── 20180616164207_add_nickname_to_team.exs │ │ ├── 20180512131400_add_claims_to_teams.exs │ │ ├── 20180527173825_add_url_data_to_message.exs │ │ ├── 20181209031824_add_public_key_to_users.exs │ │ ├── 20180427195200_create_teams.exs │ │ ├── 20180515170348_add_unique_constraint_to_team_names.exs │ │ ├── 20181222201425_add_required_to_public_key_on_users.exs │ │ ├── 20181223162110_add_rejected_to_user_requests.exs │ │ ├── 20180426162009_create_users.exs │ │ ├── 20180427194621_create_tags.exs │ │ ├── 20180603153408_addscope_and_group_pub_key_to_requests.exs │ │ ├── 20181213004517_add_encrypted_password_to_users.exs │ │ ├── 20180603151531_create_requests.exs │ │ ├── 20180427194813_create_message_tags.exs │ │ ├── 20180427194318_create_messages.exs │ │ └── 20181222211648_create_user_requests.exs │ └── seeds.exs └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── docker-compose.dev.yml ├── docker-compose.yml ├── config ├── test.exs ├── config.exs ├── dev.exs └── prod.exs ├── .gitignore ├── docker-compose.gcp.yml ├── manifest.json ├── LICENSE ├── mix.exs ├── Dockerfile ├── README.md └── mix.lock /assets/test/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | assets/node_modules 2 | _build/ 3 | deps/ 4 | -------------------------------------------------------------------------------- /assets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react", "stage-2"] 3 | } -------------------------------------------------------------------------------- /assets/test/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /scripts/attach.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker exec -it scalar_app_1 bash 3 | -------------------------------------------------------------------------------- /datamodel.mwb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenforthewin/mentat/HEAD/datamodel.mwb -------------------------------------------------------------------------------- /react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenforthewin/mentat/HEAD/react-logo.png -------------------------------------------------------------------------------- /assets/static/images/openpgp.worker.min.js: -------------------------------------------------------------------------------- 1 | ./node_modules/openpgp/dist/openpgp.worker.min.js -------------------------------------------------------------------------------- /lib/app_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.PageView do 2 | use AppWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/app_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.LayoutView do 2 | use AppWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(App.Repo, :manual) 4 | 5 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenforthewin/mentat/HEAD/assets/static/favicon.ico -------------------------------------------------------------------------------- /scripts/run_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose -f docker-compose.yml -f docker-compose.dev.yml up 3 | -------------------------------------------------------------------------------- /assets/static/images/matt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenforthewin/mentat/HEAD/assets/static/images/matt.jpg -------------------------------------------------------------------------------- /scripts/run_prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build 3 | -------------------------------------------------------------------------------- /assets/js/utils/api.js: -------------------------------------------------------------------------------- 1 | export const isAuthenticated = (userReducer) => { 2 | return !!userReducer.token 3 | } 4 | 5 | -------------------------------------------------------------------------------- /assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenforthewin/mentat/HEAD/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /test/app_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.PageViewTest do 2 | use AppWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/app_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.LayoutViewTest do 2 | use AppWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /assets/theme/site/globals/site.overrides: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Site Overrides 3 | *******************************/ 4 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | ports: 5 | - 80:4000 6 | environment: 7 | - MIX_ENV=prod 8 | - PORT=4000 -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | 5 | cd /app && mix ecto.create 6 | cd /app && mix ecto.migrate 7 | cd /app && exec mix phx.server 8 | -------------------------------------------------------------------------------- /lib/app_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.PageController do 2 | use AppWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/app_web/views/team_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.TeamView do 2 | use AppWeb, :view 3 | 4 | def render("team.json", %{team: team}) do 5 | %{uuid: team.name, nickname: team.nickname} 6 | end 7 | end -------------------------------------------------------------------------------- /priv/repo/migrations/20180428193155_add_body_index_to_tags.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddBodyIndexToTags do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:tags, [:name]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /scripts/run_gcp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose -f docker-compose.gcp.yml up --build -d 3 | docker network connect nginx_network scalar_app_1 4 | docker network connect bridge scalar_app_1 5 | docker restart scalar_app_1 -------------------------------------------------------------------------------- /priv/repo/migrations/20180428200542_add_name_index_to_teams.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddNameIndexToTeams do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:teams, [:name]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | ports: 5 | - 4000:4000 6 | - 8080:8080 7 | volumes: 8 | - $PWD:/app 9 | tty: true 10 | db: 11 | ports: 12 | - 5432:5432 -------------------------------------------------------------------------------- /assets/static/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 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180530191317_add_unique_constraint_to_tags.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddUniqueConstraintToTags do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create unique_index(:tags, [:name, :team_id]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180507182458_add_color_to_user.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddColorToUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :color, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180606185126_add_avatar_to_users.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddAvatarToUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :avatar, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/web/endpoint.ex": -------------------------------------------------------------------------------- /priv/repo/migrations/20180609181732_add_avatar_to_requests.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddAvatarToRequests do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:requests) do 6 | add :avatar, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180616164207_add_nickname_to_team.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddNicknameToTeam do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:teams) do 6 | add :nickname, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180512131400_add_claims_to_teams.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddClaimsToTeams do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:teams) do 6 | add :claim_id, :integer 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180527173825_add_url_data_to_message.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddUrlDataToMessage do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:messages) do 6 | add :url_data, :map 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20181209031824_add_public_key_to_users.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddPublicKeyToUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :public_key, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /assets/js/actions/usersAction.js: -------------------------------------------------------------------------------- 1 | export const addUser = (user) => { 2 | return { 3 | type: 'add_user', 4 | user 5 | } 6 | } 7 | 8 | export const setLastSynced = (lastSynced) => { 9 | return { 10 | type: 'set_last_synced', 11 | lastSynced 12 | } 13 | } -------------------------------------------------------------------------------- /priv/repo/migrations/20180427195200_create_teams.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.CreateTeams do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:teams) do 6 | add :name, :string 7 | 8 | timestamps() 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/app/user_manager/error_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule App.UserManager.ErrorHandler do 2 | import Plug.Conn 3 | 4 | def auth_error(conn, {type, _reason}, _opts) do 5 | body = Poison.encode!(%{error: to_string(type)}) 6 | conn 7 | |> send_resp(401, body) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/app.ex: -------------------------------------------------------------------------------- 1 | defmodule App do 2 | @moduledoc """ 3 | App keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180515170348_add_unique_constraint_to_team_names.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddUniqueConstraintToTeamNames do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop index(:teams, [:name]) 6 | 7 | create unique_index(:teams, [:name]) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20181222201425_add_required_to_public_key_on_users.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddRequiredToPublicKeyOnUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | modify :public_key, :text, null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/app/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Repo do 2 | use Ecto.Repo, otp_app: :app 3 | 4 | @doc """ 5 | Dynamically loads the repository url from the 6 | DATABASE_URL environment variable. 7 | """ 8 | def init(_, opts) do 9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20181223162110_add_rejected_to_user_requests.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddRejectedToUserRequests do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:user_requests) do 6 | add :rejected, :boolean, null: false, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/app_web/views/tag_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.TagView do 2 | use AppWeb, :view 3 | 4 | def render("index.json", %{tags: tags}) do 5 | %{tags: render_many(tags, AppWeb.TagView, "tag.json")} 6 | end 7 | 8 | def render("tag.json", %{tag: tag}) do 9 | %{name: tag.name, message_count: tag.message_count} 10 | end 11 | end -------------------------------------------------------------------------------- /priv/repo/migrations/20180426162009_create_users.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.CreateUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :name, :string 7 | add :team_id, :integer 8 | timestamps() 9 | end 10 | 11 | create index(:users, [:team_id]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180427194621_create_tags.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.CreateTags do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:tags) do 6 | add :name, :string 7 | add :team_id, :integer 8 | 9 | timestamps() 10 | end 11 | create index(:tags, [:team_id]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | .visible.transition { 2 | margin-top: auto !important; 3 | display: inline-block !important; 4 | position: relative; 5 | top: 20%; 6 | } 7 | 8 | .newTagInput input { 9 | width: 8px; 10 | -webkit-transition: width 2s; /* Safari */ 11 | transition: width 2s; 12 | } 13 | 14 | .newTagInput input:focus { 15 | width: 100px; 16 | } -------------------------------------------------------------------------------- /priv/repo/migrations/20180603153408_addscope_and_group_pub_key_to_requests.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddscopeAndGroupPubKeyToRequests do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create unique_index(:requests, [:user_id, :team_id]) 6 | alter table(:requests) do 7 | add :team_public_key, :text 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | depends_on: 6 | - db 7 | environment: 8 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 9 | db: 10 | image: postgres:9 11 | volumes: 12 | - db_store:/var/lib/postgresql/data 13 | environment: 14 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 15 | volumes: 16 | db_store: 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20181213004517_add_encrypted_password_to_users.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.AddEncryptedPasswordToUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :email, :string, null: false 7 | add :encrypted_password, :string, null: false 8 | end 9 | 10 | create unique_index(:users, [:email]) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/app_web/controllers/team_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.TeamController do 2 | use AppWeb, :controller 3 | 4 | def create(conn, %{"uuid" => uuid, "name" => nickname}) do 5 | team = %App.Team{name: uuid, nickname: nickname} 6 | team = App.Repo.insert!(team) 7 | App.Repo.insert!(%App.Tag{name: "general", team_id: team.id}) 8 | render conn, "team.json", team: team 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180603151531_create_requests.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.CreateRequests do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:requests) do 6 | add :user_public_key, :text 7 | add :encrypted_team_private_key, :text 8 | add :user_id, :integer 9 | add :team_id, :integer 10 | 11 | timestamps() 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180427194813_create_message_tags.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.CreateMessageTags do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:message_tags) do 6 | add :tag_id, :integer 7 | add :message_id, :integer 8 | 9 | timestamps() 10 | end 11 | create index(:message_tags, [:tag_id]) 12 | create index(:message_tags, [:message_id]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180427194318_create_messages.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.CreateMessages do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:messages) do 6 | add :body, :text 7 | add :team_id, :integer 8 | add :user_id, :integer 9 | 10 | timestamps() 11 | end 12 | 13 | create index(:messages, [:team_id]) 14 | create index(:messages, [:user_id]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/app/user_manager/guardian.ex: -------------------------------------------------------------------------------- 1 | defmodule App.UserManager.Guardian do 2 | use Guardian, otp_app: :app 3 | 4 | alias App.UserManager 5 | 6 | def subject_for_token(user, _claims) do 7 | {:ok, to_string(user.id)} 8 | end 9 | 10 | def resource_from_claims(%{"sub" => id}) do 11 | case UserManager.get_user!(id) do 12 | nil -> {:error, :resource_not_found} 13 | user -> {:ok, user} 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/app/message_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule App.MessageTag do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | 6 | schema "message_tags" do 7 | belongs_to :message, App.Message 8 | belongs_to :tag, App.Tag 9 | timestamps() 10 | end 11 | 12 | @doc false 13 | def changeset(message_tag, attrs) do 14 | message_tag 15 | |> cast(attrs, [:tag_id, :message_id]) 16 | |> validate_required([:tag_id, :message_id]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # App.Repo.insert!(%App.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | 14 | App.Repo.insert!(%App.Team{name: "lobby"}) 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20181222211648_create_user_requests.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Repo.Migrations.CreateUserRequests do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:user_requests) do 6 | add :public_key, :text, null: false 7 | add :encrypted_private_key, :text 8 | add :encrypted_passphrase, :text 9 | add :user_id, :integer, null: false 10 | timestamps() 11 | end 12 | 13 | create index(:user_requests, [:user_id]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /assets/js/components/TagsDropdown.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Dropdown} from 'semantic-ui-react' 3 | 4 | export default class OnlineUsersDropdown extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /lib/app_web/views/user_request_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.UserRequestView do 2 | use AppWeb, :view 3 | 4 | def render("index.json", %{user_requests: user_requests}) do 5 | %{user_requests: render_many(user_requests, AppWeb.UserRequestView, "user_request.json")} 6 | end 7 | 8 | def render("user_request.json", %{user_request: user_request}) do 9 | %{public_key: user_request.public_key, encrypted_private_key: user_request.encrypted_private_key, encrypted_passphrase: user_request.encrypted_passphrase} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/app/team.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Team do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "teams" do 6 | field :name, :string 7 | field :claim_id, :integer 8 | field :nickname, :string 9 | has_many :users, App.User 10 | has_many :messages, App.Message 11 | has_many :tags, App.Tag 12 | timestamps() 13 | end 14 | 15 | @doc false 16 | def changeset(team, attrs) do 17 | team 18 | |> cast(attrs, [:name, :claim_id, :nickname]) 19 | |> validate_required([:name]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/app_web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.UserView do 2 | use AppWeb, :view 3 | 4 | def render("index.json", %{users: users}) do 5 | %{users: render_many(users, AppWeb.UserView, "user.json")} 6 | end 7 | 8 | def render("user.json", %{user: user}) do 9 | %{uuid: user.uuid, name: user.name, avatar: user.avatar} 10 | end 11 | 12 | def render("session.json", %{jwt: jwt, id: id, name: name, color: color, public_key: public_key}) do 13 | %{jwt: jwt, id: id, name: name, color: color, public_key: public_key} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /assets/js/components/InviteUserModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import QRCode from 'qrcode' 3 | 4 | class InviteUserModal extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.qr = React.createRef(); 8 | this.qrUrl = "https://metachat.app/t/" + this.props.qrInput; 9 | } 10 | 11 | componentDidMount() { 12 | QRCode.toCanvas(this.qr.current, this.qrUrl); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | export default InviteUserModal; -------------------------------------------------------------------------------- /assets/js/components/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form } from 'semantic-ui-react' 3 | import Huebee from 'huebee' 4 | class ColorPicker extends Component { 5 | constructor(props) { 6 | super(props) 7 | } 8 | 9 | componentDidMount() { 10 | new Huebee(this.props.inputRef.current, {}) 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | ) 19 | } 20 | } 21 | 22 | export default ColorPicker; -------------------------------------------------------------------------------- /assets/js/components/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { PersistGate } from 'redux-persist/es/integration/react'; 4 | import {store, persistor, history} from '../reducers/index'; 5 | import Main from './Main' 6 | 7 | class Root extends Component { 8 | render() { 9 | return ( 10 | 11 | 12 |
13 | 14 | 15 | ); 16 | } 17 | } 18 | 19 | export default Root; -------------------------------------------------------------------------------- /lib/app_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.ErrorView do 2 | use AppWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :app, AppWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :app, App.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: System.get_env("POSTGRES_PASSWORD"), 17 | database: "app_test", 18 | hostname: "db", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /lib/app_web/views/request_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.RequestView do 2 | use AppWeb, :view 3 | 4 | def render("index.json", %{requests: requests}) do 5 | %{requests: render_many(requests, AppWeb.RequestView, "request.json")} 6 | end 7 | 8 | def render("request.json", %{request: request}) do 9 | %{encrypted_team_private_key: request.encrypted_team_private_key, user_public_key: request.user_public_key, uuid: request.user.id, name: request.user.name, avatar: request.avatar, team_name: request.team.name, team_nickname: request.team.nickname, team_public_key: request.team_public_key} 10 | end 11 | end -------------------------------------------------------------------------------- /assets/js/reducers/usersReducer.js: -------------------------------------------------------------------------------- 1 | const INITIAL_STATE = { lastSynced: null, users: {} } 2 | 3 | export default (state = INITIAL_STATE, action) => { 4 | switch (action.type) { 5 | case 'add_user': 6 | return { 7 | ...state, 8 | users: { 9 | ...state.users, 10 | [action.user.uuid]: action.user 11 | } 12 | } 13 | case 'set_last_synced': 14 | return { 15 | ...state, 16 | lastSynced: action.lastSynced 17 | } 18 | case 'burn_browser': 19 | return INITIAL_STATE; 20 | default: 21 | return state; 22 | } 23 | } -------------------------------------------------------------------------------- /lib/app/user_request.ex: -------------------------------------------------------------------------------- 1 | defmodule App.UserRequest do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "user_requests" do 6 | belongs_to :user, App.User 7 | field :public_key, :string 8 | field :encrypted_private_key, :string 9 | field :encrypted_passphrase, :string 10 | field :rejected, :boolean 11 | timestamps() 12 | end 13 | 14 | @doc false 15 | def changeset(team, attrs) do 16 | team 17 | |> cast(attrs, [:user_id, :encrypted_private_key, :encrypted_passphrase, :rejected, :public_key]) 18 | |> validate_required([:user_id, :public_key, :rejected]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/app/tag.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Tag do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | 6 | schema "tags" do 7 | field :name, :string 8 | belongs_to :team, App.Team 9 | has_many :message_tags, App.MessageTag 10 | many_to_many :messages, App.Message, join_through: "message_tags" 11 | 12 | timestamps() 13 | end 14 | 15 | @doc false 16 | def changeset(tag, attrs) do 17 | tag 18 | |> cast(attrs, [:name, :team_id]) 19 | |> validate_required([:name, :team_id]) 20 | |> validate_length(:name, min: 1) 21 | |> unique_constraint(:name, name: :tags_name_team_id_index) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /assets/js/reducers/appReducer.js: -------------------------------------------------------------------------------- 1 | import {persistCombineReducers} from 'redux-persist'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import storage from 'redux-persist/es/storage'; 4 | import userReducer from "./userReducer"; 5 | import cryptoReducer from './cryptoReducer' 6 | import messageReducer from './messageReducer' 7 | import usersReducer from './usersReducer' 8 | const config = { 9 | key: 'root', 10 | storage, 11 | }; 12 | 13 | const appReducer = persistCombineReducers(config, { 14 | routerReducer, 15 | userReducer, 16 | cryptoReducer, 17 | messageReducer, 18 | usersReducer 19 | }); 20 | 21 | export default appReducer; -------------------------------------------------------------------------------- /lib/app/message.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Message do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "messages" do 6 | field :body, :string 7 | field :url_data, :map 8 | has_many :message_tags, App.MessageTag 9 | many_to_many :tags, App.Tag, join_through: "message_tags" 10 | belongs_to :user, App.User 11 | belongs_to :team, App.Team 12 | timestamps() 13 | end 14 | 15 | @doc false 16 | def changeset(message, attrs) do 17 | message 18 | |> cast(attrs, [:body, :team_id, :user_id, :url_data]) 19 | |> validate_required([:body, :team_id, :user_id]) 20 | |> validate_length(:body, min: 1) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/app/user_manager/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule App.UserManager.Pipeline do 2 | use Guardian.Plug.Pipeline, 3 | otp_app: :app, 4 | error_handler: App.UserManager.ErrorHandler, 5 | module: App.UserManager.Guardian 6 | 7 | # If there is a session token, restrict it to an access token and validate it 8 | plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"} 9 | # If there is an authorization header, restrict it to an access token and validate it 10 | plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"} 11 | # Load the user if either of the verifications worked 12 | plug Guardian.Plug.LoadResource, allow_blank: true 13 | end 14 | -------------------------------------------------------------------------------- /assets/js/reducers/index.js: -------------------------------------------------------------------------------- 1 | import createHistory from 'history/createBrowserHistory' 2 | import { routerMiddleware } from 'react-router-redux' 3 | import { persistStore, persistCombineReducers } from 'redux-persist'; 4 | import thunk from 'redux-thunk'; 5 | import { createStore, applyMiddleware } from 'redux'; 6 | import appReducer from './appReducer'; 7 | export const history = createHistory(); 8 | 9 | const middleware = routerMiddleware(history); 10 | const reducer = appReducer; 11 | 12 | export const store = createStore( 13 | reducer, 14 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), 15 | applyMiddleware(middleware, thunk) 16 | ); 17 | 18 | export const persistor = persistStore(store); 19 | -------------------------------------------------------------------------------- /lib/app/request.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Request do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | 6 | schema "requests" do 7 | field :encrypted_team_private_key, :string 8 | field :user_public_key, :string 9 | field :team_public_key, :string 10 | field :avatar, :string 11 | belongs_to :user, App.User 12 | belongs_to :team, App.Team 13 | timestamps() 14 | end 15 | 16 | @doc false 17 | def changeset(request, attrs) do 18 | request 19 | |> cast(attrs, [:user_public_key, :encrypted_team_private_key, :user_id, :team_id, :team_public_key, :avatar]) 20 | |> validate_required([:user_public_key, :user_id, :team_id]) 21 | |> unique_constraint(:user_id, name: :requests_user_id_team_id_index) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /assets/node_modules 15 | 16 | # Since we are building assets from assets/, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # Files matching config/*.secret.exs pattern contain sensitive 22 | # data and you should not commit them into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets files as long as you replace their contents by environment 26 | # variables. 27 | /config/*.secret.exs 28 | .env 29 | db-key.json 30 | -------------------------------------------------------------------------------- /lib/app/user_manager/user_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule App.UserManager do 2 | @moduledoc """ 3 | The UserManager context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias App.Repo 8 | alias App.User 9 | alias Comeonin.Bcrypt 10 | 11 | def get_user!(id), do: Repo.get!(User, id) 12 | 13 | def authenticate_user(email, plain_text_password) do 14 | query = from u in User, where: u.email == ^email 15 | case Repo.one(query) do 16 | nil -> 17 | Bcrypt.dummy_checkpw() 18 | {:error, :invalid_credentials} 19 | user -> 20 | if Bcrypt.checkpw(plain_text_password, user.encrypted_password) do 21 | {:ok, user} 22 | else 23 | {:error, :invalid_credentials} 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /docker-compose.gcp.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | depends_on: 6 | - db 7 | environment: 8 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 9 | - MIX_ENV=prod 10 | - PORT=4000 11 | - LETSENCRYPT_HOST=${LETSENCRYPT_HOST} 12 | - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL} 13 | - VIRTUAL_HOST=${VIRTUAL_HOST} 14 | - FULL_HOST=${FULL_HOST} 15 | db: 16 | image: gcr.io/cloudsql-docker/gce-proxy:1.11 17 | environment: 18 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 19 | volumes: 20 | - $PWD/cloudsql:/cloudsql 21 | - $PWD/db-key.json:/config 22 | command: 23 | - /cloud_sql_proxy 24 | - -instances=react-rails-auth:us-east1:scalr3-east=tcp:0.0.0.0:5432 25 | - -credential_file=/config 26 | -------------------------------------------------------------------------------- /lib/app_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Metachat 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/app_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import AppWeb.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :app 24 | end 25 | -------------------------------------------------------------------------------- /assets/js/actions/messageActions.js: -------------------------------------------------------------------------------- 1 | export const addMessage = (message) => { 2 | return { 3 | type: 'add_message', 4 | id: message.id, 5 | message 6 | } 7 | }; 8 | 9 | export const newUrl = (id, urlData, tag) => { 10 | return (dispatch, _) => { 11 | dispatch({ 12 | type: 'new_url', 13 | id, 14 | urlData 15 | }) 16 | 17 | if (tag) { 18 | dispatch({ 19 | type: 'new_tag', 20 | id, 21 | tag 22 | }) 23 | } 24 | } 25 | }; 26 | 27 | export const newTag = (id, tag) => { 28 | return { 29 | type: 'new_tag', 30 | id, 31 | tag 32 | } 33 | }; 34 | 35 | export const refreshTags = (id, tags) => { 36 | return { 37 | type: 'update_tags', 38 | id, 39 | tags 40 | } 41 | } 42 | 43 | export const removeTag = (id, tag) => { 44 | return { 45 | type: 'remove_tag', 46 | id, 47 | tag 48 | } 49 | } -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "phoenix_html" 15 | import "../theme/semantic.less"; 16 | import '../node_modules/huebee/dist/huebee.css' 17 | import '../css/app.css'; 18 | import React from 'react' 19 | import ReactDOM from 'react-dom' 20 | import Root from './components/Root' 21 | import registerServiceWorker from './registerServiceWorker'; 22 | 23 | ReactDOM.render(, document.getElementById('root')) 24 | registerServiceWorker(); -------------------------------------------------------------------------------- /lib/app_web/views/message_view.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.MessageView do 2 | use AppWeb, :view 3 | 4 | def render("index.json", %{messages: messages}) do 5 | %{messages: render_many(messages, AppWeb.MessageView, "message.json")} 6 | end 7 | 8 | def render("show.json", %{message: message}) do 9 | %{message: render_one(message, AppWeb.MessageView, "message.json")} 10 | end 11 | 12 | def render("message.json", %{message: message}) do 13 | %{id: message.id, body: message.body, inserted_at: message.inserted_at, tags: render_many(message.tags, AppWeb.MessageView, "message_tag.json"), user: render_one(message.user, AppWeb.MessageView, "message_user.json"), url_data: message.url_data} 14 | end 15 | 16 | def render("message_tag.json", tag) do 17 | %{name: tag.message.name} 18 | end 19 | 20 | def render("message_user.json", user) do 21 | %{name: user.message.name, color: user.message.color, uuid: user.message.id} 22 | end 23 | end -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Groupchat", 3 | "short_name": "Groupchat", 4 | "start_url": ".", 5 | "description": "Group chat with tags and pretty good privacy.", 6 | "display": "standalone", 7 | "icons": [{ 8 | "src": "/images/react-logo.png", 9 | "sizes": "48x48", 10 | "type": "image/png" 11 | }, { 12 | "src": "/images/react-logo.png", 13 | "sizes": "72x72", 14 | "type": "image/png" 15 | }, { 16 | "src": "/images/react-logo.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, { 20 | "src": "/images/react-logo.png", 21 | "sizes": "144x144", 22 | "type": "image/png" 23 | }, { 24 | "src": "/images/react-logo.png", 25 | "sizes": "168x168", 26 | "type": "image/png" 27 | }, { 28 | "src": "/images/react-logo.png", 29 | "sizes": "192x192", 30 | "type": "image/png" 31 | },{ 32 | "src": "/images/react-logo.png", 33 | "sizes": "512x512", 34 | "type": "image/png" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint AppWeb.Endpoint 25 | end 26 | end 27 | 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(App.Repo) 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(App.Repo, {:shared, self()}) 33 | end 34 | :ok 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kenneth Bergquist 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 | -------------------------------------------------------------------------------- /lib/app_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.Router do 2 | use AppWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | pipeline :auth do 17 | plug App.UserManager.Pipeline 18 | end 19 | 20 | pipeline :ensure_auth do 21 | plug Guardian.Plug.EnsureAuthenticated 22 | end 23 | 24 | scope "/auth", AppWeb do 25 | pipe_through [:api, :auth] 26 | 27 | post "/login", UserController, :login 28 | post "/sign_up", UserController, :create 29 | end 30 | 31 | scope "/api", AppWeb, as: :api do 32 | pipe_through [:api, :auth, :ensure_auth] 33 | 34 | resources "/teams", TeamController 35 | end 36 | 37 | scope "/", AppWeb do 38 | pipe_through :browser # Use the default browser stack 39 | 40 | get "/*path", PageController, :index 41 | end 42 | 43 | # Other scopes may use custom stacks. 44 | # scope "/api", AppWeb do 45 | # pipe_through :api 46 | # end 47 | end 48 | -------------------------------------------------------------------------------- /lib/app_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= gettext "Welcome to %{name}!", name: "Phoenix" %>

3 |

A productive web framework that
does not compromise speed and maintainability.

4 |
5 | 6 |
7 |
8 |

Resources

9 | 20 |
21 | 22 |
23 |

Help

24 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /lib/app/application.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Application do 2 | use Application 3 | 4 | # See https://hexdocs.pm/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | supervisor(App.Repo, []), 13 | # Start the endpoint when the application starts 14 | supervisor(AppWeb.Endpoint, []), 15 | supervisor(AppWeb.Presence, []) 16 | # Start your own worker by calling: App.Worker.start_link(arg1, arg2, arg3) 17 | # worker(App.Worker, [arg1, arg2, arg3]), 18 | ] 19 | 20 | # See https://hexdocs.pm/elixir/Supervisor.html 21 | # for other strategies and supported options 22 | opts = [strategy: :one_for_one, name: App.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | # Tell Phoenix to update the endpoint configuration 27 | # whenever the application is updated. 28 | def config_change(changed, _new, removed) do 29 | AppWeb.Endpoint.config_change(changed, removed) 30 | :ok 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :app, 10 | ecto_repos: [App.Repo] 11 | 12 | # Configures the endpoint 13 | config :app, AppWeb.Endpoint, 14 | url: [host: "localhost"], 15 | secret_key_base: "dEuF49PzQHWcYkUeEiswToC575o5KGohPhcKvL/kt8ZebaA3HC9ziJOkPf2oLG0e", 16 | render_errors: [view: AppWeb.ErrorView, accepts: ~w(html json)], 17 | pubsub: [name: App.PubSub, 18 | adapter: Phoenix.PubSub.PG2] 19 | 20 | # Configures Elixir's Logger 21 | config :logger, :console, 22 | format: "$time $metadata[$level] $message\n", 23 | metadata: [:user_id] 24 | 25 | config :app, App.UserManager.Guardian, 26 | issuer: "app", 27 | secret_key: "wG2uSgkaxW+Bg0tytAvdtr2k863DzJjaIEE1MuB1yjyoaX1O+Snh8u0CwCkiv2t2" 28 | 29 | # Import environment specific config. This must remain at the bottom 30 | # of this file so it overrides the configuration defined above. 31 | import_config "#{Mix.env}.exs" 32 | -------------------------------------------------------------------------------- /assets/js/reducers/cryptoReducer.js: -------------------------------------------------------------------------------- 1 | const INITIAL_STATE = { 2 | publicKey: null, 3 | privateKey: null, 4 | passphrase: null, 5 | groups: { } 6 | } 7 | 8 | export default (state = INITIAL_STATE, action) => { 9 | switch (action.type) { 10 | case 'new_key': 11 | return { 12 | ...state, 13 | passphrase: action.passphrase, 14 | privateKey: action.privateKey, 15 | publicKey: action.publicKey 16 | } 17 | case 'new_group_key': 18 | return { 19 | ...state, 20 | groups: { 21 | ...state.groups, 22 | [action.room]: { 23 | privateKey: action.privateKey, 24 | publicKey: action.publicKey, 25 | nickname: action.name || '' 26 | } 27 | } 28 | } 29 | case 'new_group_name': 30 | return { 31 | ...state, 32 | groups: { 33 | ...state.groups, 34 | [action.room]: { 35 | ...state.groups[action.room], 36 | nickname: action.nickname 37 | } 38 | } 39 | } 40 | case 'burn_browser': 41 | return INITIAL_STATE; 42 | default: 43 | return state; 44 | } 45 | } -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import AppWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint AppWeb.Endpoint 26 | end 27 | end 28 | 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(App.Repo) 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(App.Repo, {:shared, self()}) 34 | end 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/app/user.ex: -------------------------------------------------------------------------------- 1 | defmodule App.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias Comeonin.Bcrypt 5 | 6 | schema "users" do 7 | field :name, :string 8 | field :color, :string 9 | field :avatar, :string 10 | field :public_key, :string 11 | field :email, :string 12 | field :encrypted_password, :string 13 | belongs_to :team, App.Team 14 | has_many :messages, App.Message 15 | has_many :user_requests, App.UserRequest 16 | timestamps() 17 | end 18 | 19 | @doc false 20 | def changeset(user, attrs) do 21 | user 22 | |> cast(attrs, [:name, :team_id, :color, :avatar, :public_key, :email, :encrypted_password]) 23 | |> validate_required([:email, :encrypted_password, :public_key]) 24 | |> validate_format(:email, ~r/\A[^@\s]+@([^@\s]+\.)+[^@\W]+\z/) 25 | |> validate_length(:encrypted_password, min: 8) 26 | |> unique_constraint(:email) 27 | |> put_password_hash() 28 | end 29 | 30 | defp put_password_hash(%Ecto.Changeset{valid?: true, changes: %{encrypted_password: encrypted_password}} = changeset) do 31 | change(changeset, encrypted_password: Bcrypt.hashpwsalt(encrypted_password)) 32 | end 33 | 34 | defp put_password_hash(changeset), do: changeset 35 | end 36 | -------------------------------------------------------------------------------- /assets/js/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | const INITIAL_STATE = { 2 | uuid: null, 3 | name: '', 4 | color: '#'+Math.floor(Math.random()*16777215).toString(16), 5 | urlPreviews: true, 6 | token: null, 7 | authErrors: {} 8 | } 9 | 10 | export default (state = INITIAL_STATE, action) => { 11 | switch (action.type) { 12 | case '@@router/LOCATION_CHANGE': 13 | return { 14 | ...state, 15 | authErrors: {} 16 | } 17 | case 'expire_token': 18 | return { 19 | ...state, 20 | token: null 21 | } 22 | case 'set_name': 23 | return { 24 | ...state, 25 | name: action.name, 26 | color: action.color 27 | } 28 | case 'auth_errors': 29 | return { 30 | ...state, 31 | authErrors: action.errors 32 | } 33 | case 'burn_browser': 34 | return INITIAL_STATE; 35 | case 'set_url_previews': 36 | return { 37 | ...state, 38 | urlPreviews: action.urlPreviews 39 | } 40 | case 'sign_in': 41 | return { 42 | ...state, 43 | token: action.token, 44 | uuid: action.id, 45 | color: action.color, 46 | name: action.name, 47 | authErrors: {} 48 | } 49 | default: 50 | return state; 51 | } 52 | } -------------------------------------------------------------------------------- /assets/js/components/RenderedUrl.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { Item } from 'semantic-ui-react'; 3 | 4 | export default class RenderedUrl extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.maybeRenderImage = this.maybeRenderImage.bind(this); 8 | } 9 | 10 | maybeRenderImage(url, image = false) { 11 | if(url && url.startsWith('https')) { 12 | const size = image ? 'large' : 'small' 13 | return ( 14 | 15 | ); 16 | } 17 | 18 | return null; 19 | } 20 | 21 | render() { 22 | const data = this.props.urlData; 23 | if(data.content_type.startsWith('image')) { 24 | return ( 25 | 26 | 27 | {this.maybeRenderImage(data.url, true)} 28 | 29 | 30 | ); 31 | } else if (data.content_type.startsWith('text/html')) { 32 | return ( 33 | 34 | 35 | {this.maybeRenderImage(data.image)} 36 | 37 | {data.title} 38 | {data.description} 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/app_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | channel "room:*", AppWeb.RoomChannel 6 | channel "user:*", AppWeb.UserChannel 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(%{"token" => token}, socket) do 23 | case Guardian.Phoenix.Socket.authenticate(socket, App.UserManager.Guardian, token) do 24 | {:ok, authed_socket} -> 25 | {:ok, authed_socket} 26 | {:error, _} -> :error 27 | end 28 | end 29 | 30 | # Socket id's are topics that allow you to identify all sockets for a given user: 31 | # 32 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 33 | # 34 | # Would allow you to broadcast a "disconnect" event and terminate 35 | # all active sockets and channels for a given user: 36 | # 37 | # AppWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 38 | # 39 | # Returning `nil` makes this socket anonymous. 40 | def id(_socket), do: nil 41 | end 42 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule App.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias App.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import App.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(App.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(App.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transform changeset errors to a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/app_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) -> 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # When using gettext, we typically pass the strings we want 22 | # to translate as a static argument: 23 | # 24 | # # Translate "is invalid" in the "errors" domain 25 | # dgettext "errors", "is invalid" 26 | # 27 | # # Translate the number of files with plural rules 28 | # dngettext "errors", "1 file", "%{count} files", count 29 | # 30 | # Because the error messages we show in our forms and APIs 31 | # are defined inside Ecto, we need to translate them dynamically. 32 | # This requires us to call the Gettext module passing our gettext 33 | # backend as first argument. 34 | # 35 | # Note we use the "errors" domain, which means translations 36 | # should be written to the errors.po file. The :count option is 37 | # set by Ecto and indicates we should also apply plural rules. 38 | if count = opts[:count] do 39 | Gettext.dngettext(AppWeb.Gettext, "errors", msg, msg, count, opts) 40 | else 41 | Gettext.dgettext(AppWeb.Gettext, "errors", msg, opts) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /assets/js/components/ExportKey.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Segment, Button, Icon, Message, Container } from 'semantic-ui-react' 3 | import { connect } from 'react-redux' 4 | import { Redirect } from 'react-router-dom' 5 | import { saveAs } from 'file-saver'; 6 | 7 | class ExportKey extends Component { 8 | constructor(props) { 9 | super(props) 10 | 11 | this.onClick = this.onClick.bind(this) 12 | } 13 | 14 | onClick() { 15 | const { publicKey, privateKey, passphrase } = this.props.cryptoReducer 16 | const keyObject = { publicKey, privateKey, passphrase } 17 | const keyBlob = new Blob([JSON.stringify(keyObject)], {type: "text/plain;charset=utf-8"}); 18 | saveAs(keyBlob, "chat-key.json"); 19 | } 20 | 21 | render() { 22 | if (!this.props.userReducer.token) { 23 | return ( ) 24 | } 25 | return ( 26 | 27 |
28 | 29 | 30 | Export key 31 |

32 | Press the button below to export your private account key to a file. Keep this file safe and secure: you can use it to restore your account if you ever lose access. 33 |

34 |
35 |
36 | 37 |
38 |
39 | ) 40 | } 41 | } 42 | 43 | const mapStateToProps = (state) => { 44 | const { userReducer, cryptoReducer } = state 45 | return { userReducer, cryptoReducer } 46 | } 47 | 48 | export default connect(mapStateToProps, {})(ExportKey) -------------------------------------------------------------------------------- /lib/app_web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.UserController do 2 | use AppWeb, :controller 3 | alias App.{UserManager, UserManager.Guardian, User, Repo} 4 | import AppWeb.ErrorHelpers 5 | 6 | def login(conn, %{"email" => email, "password" => password}) do 7 | UserManager.authenticate_user(email, password) 8 | |> login_reply(conn) 9 | end 10 | 11 | def create(conn, %{"email" => email, "password" => password, "publicKey" => public_key, "color" => color}) do 12 | Repo.insert(User.changeset(%User{}, %{encrypted_password: password, email: email, public_key: public_key, color: color, name: email})) 13 | |> create_reply(conn, password) 14 | end 15 | 16 | defp create_reply({:ok, user}, conn, password) do 17 | UserManager.authenticate_user(user.email, password) 18 | |> login_reply(conn) 19 | end 20 | 21 | defp create_reply({:error, user}, conn, _) do 22 | conn 23 | |> put_status(:bad_request) 24 | |> json(%{errors: Ecto.Changeset.traverse_errors(user, &translate_error/1)}) 25 | end 26 | 27 | # def logout(conn, _) do 28 | # conn 29 | # |> Guardian.Plug.sign_out() 30 | # |> redirect(to: "/login") 31 | # end 32 | 33 | defp login_reply({:ok, user}, conn) do 34 | conn = Guardian.Plug.sign_in(conn, user) 35 | jwt = Guardian.Plug.current_token(conn) 36 | claims = Guardian.Plug.current_claims(conn) 37 | render(conn, "session.json", jwt: jwt, id: user.id, name: user.name, color: user.color, public_key: user.public_key) 38 | end 39 | 40 | defp login_reply({:error, reason}, conn) do 41 | conn 42 | |> put_status(:bad_request) 43 | |> json(%{error: reason}) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/app_web.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use AppWeb, :controller 9 | use AppWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: AppWeb 23 | import Plug.Conn 24 | import AppWeb.Router.Helpers 25 | import AppWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, root: "lib/app_web/templates", 32 | namespace: AppWeb 33 | 34 | # Import convenience functions from controllers 35 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 36 | 37 | # Use all HTML functionality (forms, tags, etc) 38 | use Phoenix.HTML 39 | 40 | import AppWeb.Router.Helpers 41 | import AppWeb.ErrorHelpers 42 | import AppWeb.Gettext 43 | end 44 | end 45 | 46 | def router do 47 | quote do 48 | use Phoenix.Router 49 | import Plug.Conn 50 | import Phoenix.Controller 51 | end 52 | end 53 | 54 | def channel do 55 | quote do 56 | use Phoenix.Channel 57 | import AppWeb.Gettext 58 | end 59 | end 60 | 61 | @doc """ 62 | When used, dispatch to the appropriate controller/view/etc. 63 | """ 64 | defmacro __using__(which) when is_atom(which) do 65 | apply(__MODULE__, which, []) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/app_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule AppWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :app 3 | 4 | socket "/socket", AppWeb.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :app, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt service-worker.js manifest.json) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.Logger 23 | 24 | plug Plug.Parsers, 25 | parsers: [:urlencoded, :multipart, :json], 26 | pass: ["*/*"], 27 | json_decoder: Poison 28 | 29 | plug Plug.MethodOverride 30 | plug Plug.Head 31 | 32 | # The session will be stored in the cookie and signed, 33 | # this means its contents can be read but not tampered with. 34 | # Set :encryption_salt if you would also like to encrypt it. 35 | plug Plug.Session, 36 | store: :cookie, 37 | key: "_app_key", 38 | signing_salt: "KYGrpENc" 39 | 40 | plug AppWeb.Router 41 | 42 | @doc """ 43 | Callback invoked for dynamically configuring the endpoint. 44 | 45 | It receives the endpoint configuration and checks if 46 | configuration should be loaded from the system environment. 47 | """ 48 | def init(_key, config) do 49 | if config[:load_from_system_env] do 50 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 51 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 52 | else 53 | {:ok, config} 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /assets/js/reducers/messageReducer.js: -------------------------------------------------------------------------------- 1 | const INITIAL_STATE = { 2 | messages: { 3 | 4 | } 5 | } 6 | 7 | export default (state = INITIAL_STATE, action) => { 8 | switch (action.type) { 9 | case 'add_message': 10 | return { 11 | ...state, 12 | messages: { 13 | ...state.messages, 14 | [action.id]: { 15 | ...state.messages[action.id], 16 | ...action.message, 17 | tags: [ 18 | ...action.message.tags, 19 | ...(state.messages[action.id] && state.messages[action.id].tags ? state.messages[action.id].tags : []) 20 | ] 21 | } 22 | } 23 | } 24 | case 'new_url': 25 | return { 26 | ...state, 27 | messages: { 28 | ...state.messages, 29 | [action.id]: { 30 | ...state.messages[action.id], 31 | urlData: action.urlData 32 | } 33 | } 34 | } 35 | case 'new_tag': 36 | return { 37 | ...state, 38 | messages: { 39 | ...state.messages, 40 | [action.id]: { 41 | ...state.messages[action.id], 42 | tags: [ 43 | ...(state.messages[action.id].tags || []), 44 | action.tag 45 | ] 46 | } 47 | } 48 | } 49 | case 'update_tags': 50 | return { 51 | ...state, 52 | messages: { 53 | ...state.messages, 54 | [action.id]: { 55 | ...state.messages[action.id], 56 | tags: action.tags 57 | } 58 | } 59 | }; 60 | case 'remove_tag': 61 | return { 62 | ...state, 63 | messages: { 64 | ...state.messages, 65 | [action.id]: { 66 | ...state.messages[action.id], 67 | tags: state.messages[action.id].tags.filter(e => e !== action.tag) 68 | } 69 | } 70 | }; 71 | case 'burn_browser': 72 | return INITIAL_STATE; 73 | default: 74 | return state; 75 | } 76 | } -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :app, 7 | version: "0.0.1", 8 | elixir: "~> 1.4", 9 | elixirc_paths: elixirc_paths(Mix.env), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {App.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.3.2"}, 37 | {:phoenix_pubsub, "~> 1.0"}, 38 | {:phoenix_ecto, "~> 3.2"}, 39 | {:postgrex, ">= 0.0.0"}, 40 | {:phoenix_html, "~> 2.10"}, 41 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 42 | {:gettext, "~> 0.11"}, 43 | {:cowboy, "~> 1.0"}, 44 | {:scrape, "~> 2.0"}, 45 | {:html5ever, "~> 0.5.0", override: true}, 46 | {:rustler, "~> 0.16.0", override: true}, 47 | {:guardian, "~> 1.0"}, 48 | {:comeonin, "~> 4.0"}, 49 | {:bcrypt_elixir, "~> 1.1"} 50 | ] 51 | end 52 | 53 | # Aliases are shortcuts or tasks specific to the current project. 54 | # For example, to create, migrate and run the seeds file at once: 55 | # 56 | # $ mix ecto.setup 57 | # 58 | # See the documentation for `Mix` for more info on aliases. 59 | defp aliases do 60 | [ 61 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 62 | "ecto.reset": ["ecto.drop", "ecto.setup"], 63 | "test": ["ecto.create --quiet", "ecto.migrate", "test"] 64 | ] 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /assets/js/components/MessageForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Form, TextArea, Icon} from 'semantic-ui-react' 3 | import 'emoji-mart/css/emoji-mart.css' 4 | import { Picker } from 'emoji-mart' 5 | 6 | export default class MessageForm extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { pickerVisible: false } 10 | 11 | this.maybeRenderPicker = this.maybeRenderPicker.bind(this); 12 | this.addEmoji = this.addEmoji.bind(this); 13 | this.togglePicker = this.togglePicker.bind(this); 14 | 15 | this.formStyles = { 16 | // flex: '0 1 auto', 17 | marginBottom: '50px', 18 | // display: 'flex', 19 | // alignItems: 'center', 20 | // height: '100%', 21 | // minHeight: '2.71428571em' 22 | } 23 | } 24 | 25 | maybeRenderPicker() { 26 | // if (!this.state.pickerVisible) return; 27 | 28 | return ( 29 | 30 | ); 31 | } 32 | 33 | addEmoji(emoji) { 34 | this.props.textAreaNode.current.ref.value = this.props.textAreaNode.current.ref.value + emoji.native; 35 | this.props.textAreaNode.current.ref.focus(); 36 | 37 | this.setState({ 38 | ...this.state, 39 | pickerVisible: false 40 | }) 41 | } 42 | 43 | togglePicker(e) { 44 | e.preventDefault(); 45 | this.setState({ 46 | ...this.state, 47 | pickerVisible: !this.state.pickerVisible 48 | }) 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 |