Compare commits

...

No commits in common. "develop" and "master" have entirely different histories.

171 changed files with 3222 additions and 9899 deletions

View File

@ -1,38 +1,5 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
ketbin-*.tar
# If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
/target
.idea
/native/ketbin_utils_syntax/target
/native/ketbin_utils_syntax/.cargo
.elixir_ls
.env
.cargo
*.dump

View File

@ -1,5 +0,0 @@
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]

View File

@ -1,12 +0,0 @@
name: Fly Deploy
on: [push]
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
deploy:
name: Deploy to fly.io
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only

42
.gitignore vendored
View File

@ -1,41 +1,5 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
ketbin-*.tar
# If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
/priv/native/
/target
.idea
.env
.envrc
# Secret config files
config/*.secret.exs
.cargo
*.dump

2068
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

41
Cargo.toml Normal file
View File

@ -0,0 +1,41 @@
[package]
name = "katbin"
version = "0.1.0"
authors = ["ATechnoHazard <amolele@gmail.com>"]
license = "MIT"
description = "Small, lightweight pastebin+URL shortener written in Rust."
repository = "https://github.com/ATechnoHazard/katbin"
homepage = "https://github.com/ATechnoHazard/katbin"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = "0.4.10"
rocket_contrib = { version = "0.4.10", default-features = false, features = ["json"] }
serde = "1.0.126"
serde_json = "1.0.64"
serde_derive = "1.0.126"
diesel = { version = "1.4.6", features = ["postgres", "r2d2", "chrono"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
jsonwebtoken = "7.2.0"
slog = "2.7.0"
slog-term = "2.8.0"
anyhow = "1.0.40"
slog-async = "2.6.0"
bcrypt = "0.9.0"
chrono = "0.4.19"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
jirachi = { version = "0.1.9", features = ["collision-resistant"] }
rocket_cors = "0.5.2"
regex = "1.5.4"
[dev-dependencies]
diesel_cli = { version = "1.4.1", default-features = false, features = ["postgres"] }
jirachi_cli = "0.1.2"
[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"

View File

@ -1,54 +1,19 @@
FROM elixir:alpine AS build
FROM rustlang/rust:nightly AS builder
# install build dependencies
RUN apk add --no-cache build-base npm git curl py-pip rust cargo
COPY . .
# prepare build dir
WORKDIR /app
RUN cargo build --release
# install hex + rebar
RUN mix local.hex --force && \
mix local.rebar --force
# CMD ["sh", "-c", "ROCKET_KEEP_ALIVE=0 ./target/release/katbin"]
# set build ENV
ENV MIX_ENV=prod
FROM debian:buster
# install mix dependencies
COPY mix.exs mix.lock ./
COPY config config
RUN mix do deps.get, deps.compile
RUN apt-get update && apt-get install -y libpq-dev
# build assets
COPY assets/package.json assets/package-lock.json ./assets/
RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
COPY --from=builder \
/target/release/katbin\
/usr/local/bin/
COPY priv priv
COPY assets assets
COPY lib lib
COPY native native
RUN npm run --prefix ./assets deploy
RUN mix phx.digest
WORKDIR /root
# uncomment COPY if rel/ exists
# COPY rel rel
RUN mix do compile, release
# prepare release image
FROM alpine AS app
RUN apk add --no-cache openssl ncurses-libs libstdc++
WORKDIR /app
RUN chown nobody:nobody /app
USER nobody:nobody
COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/ketbin ./
COPY --chown=nobody:nobody startup.sh startup.sh
RUN chmod +x /app/startup.sh
ENV HOME=/app
CMD ["/app/startup.sh"]
CMD /usr/local/bin/katbin

8
LICENSE Normal file
View File

@ -0,0 +1,8 @@
Copyright 2020 SphericalKat
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,7 +0,0 @@
.PHONY: image
image:
docker build -t atechnohazard/katbin-elixir .
.PHONY: push
push: image
docker push atechnohazard/katbin-elixir

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: ROCKET_PORT=$PORT ROCKET_KEEP_ALIVE=0 ./target/release/katbin

View File

@ -1,52 +1,2 @@
# Katbin
To start your Phoenix server:
* Copy `config/dev.secret.sample.exs` to `config/dev.secret.exs`
* Fill in the SMTP and database configuration in `config/dev.secret.exs`
* Install dependencies with `mix deps.get`
* Create and migrate your database with `mix ecto.setup`
* Install Node.js dependencies with `npm install` inside the `assets` directory
* Start Phoenix endpoint with `mix phx.server`
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
# Self hosting
We recommend using our [official docker image](https://hub.docker.com/r/atechnohazard/katbin-elixir). If you wish to build your own docker image, the provided Dockerfile should work out of the box. If you run into any problems, please open an issue.
## Populating the environment
Copy the provided `sample.env` file to `.env`
```sh
cp sample.env .env
```
Fill in this new file with the required environment variables.
## Using the official docker image
```sh
docker run --env-file .env atechnohazard/katbin-elixir
```
## Building the docker image
```sh
git clone https://github.com/SphericalKat/katbin
cd katbin
docker build -t <username>/katbin .
```
## Running the built docker image
```sh
docker run --env-file .env <username>/katbin
```
For other methods of self hosting, please check the [official Phoenix deployment guides](https://hexdocs.pm/phoenix/deployment.html).
## Learn more
* Official website: https://www.phoenixframework.org/
* Guides: https://hexdocs.pm/phoenix/overview.html
* Docs: https://hexdocs.pm/phoenix
* Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix
# katbin
Small, lightweight pastebin+URL shortener written in Rust.

23
Rocket.toml Normal file
View File

@ -0,0 +1,23 @@
[development]
address = "localhost"
port = 8000
keep_alive = 5
log = "normal"
limits = { forms = 32768 }
[staging]
address = "0.0.0.0"
port = 8000
keep_alive = 5
log = "normal"
limits = { forms = 32768 }
[production]
address = "0.0.0.0"
port = 8000
keep_alive = 5
log = "critical"
limits = { forms = 32768 }
#[global]
#template_dir = "resources/templates/"

View File

@ -1,5 +0,0 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@ -1,38 +0,0 @@
const fs = require("fs-extra");
const esbuild = require("esbuild");
const { default: postCSSPlugin } = require("esbuild-plugin-postcss2");
// PostCSS Plugins
const autoprefixer = require("autoprefixer");
const tailwindcss = require("tailwindcss");
const postcssImport = require("postcss-import");
const productionBuild = process.env.NODE_ENV === "production";
fs.copySync("static/", "../priv/static/");
console.log("[build.js] [info] Copying static files from static/");
if (!productionBuild) {
console.log("[build.js] [info] Starting to watch assets for changes");
} else {
console.log("[build.js] [info] Building assets in production mode");
}
esbuild
.build({
entryPoints: ["js/app.js"],
bundle: true,
outfile: "../priv/static/assets/app.js",
minify: productionBuild,
watch: !productionBuild,
external: ["*.ttf"],
plugins: [
postCSSPlugin({
plugins: [postcssImport, tailwindcss, autoprefixer],
}),
],
})
.catch((e) => {
console.error(`[build.js] [error] ${e}`);
process.exit(1);
});

View File

@ -1,128 +0,0 @@
/* This file is for your main application css. */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "./highlight.css";
@import "./markdown.css";
@font-face {
font-family: "JetBrains Mono";
src: url(/fonts/jetbrains_mono_variable.ttf);
font-weight: 100 1000;
font-stretch: 25% 151%;
}
* {
font-family: "JetBrains Mono", monospace;
color: white;
font-size: 14px;
font-weight: bold;
}
@layer base {
h1 {
font-size: 2em;
font-weight: bold;
}
h2 {
font-size: 1.5em;
font-weight: bold;
}
h3 {
font-size: 1.17em;
font-weight: bold;
}
h4 {
font-weight: bold;
}
h5 {
font-size: 0.83em;
font-weight: bold;
}
h6 {
font-size: 0.67em;
font-weight: bold;
}
h1,
h2,
h3 {
margin-top: 20px;
margin-bottom: 10px
}
img {
display: inline;
}
}
html,
body {
width: 100%;
height: 100%;
}
a {
color: #ff9800;
}
a:hover {
color: #ff9800;
text-decoration: underline;
}
header {
background-color: #1a1a1a;
}
code {
font-family: "JetBrains Mono", monospace;
}
code {
white-space: pre;
font-size: 14px;
font-weight: bold;
}
textarea {
font-size: 14px;
font-weight: bold;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-corner,
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 8px;
@apply bg-amber;
}
.alert {
width: 100%;
text-align: center;
}
.alert-info {
background-color: #1ed98e;
color: black;
font-weight: bold;
}
.alert-danger {
background-color: #ff9800;
color: black;
font-weight: bold;
}

View File

@ -1,239 +0,0 @@
.code {
color: #c0c5ce;
background-color: #2b303b;
}
.variable.parameter.function {
color: #c0c5ce;
}
.comment {
color: #65737e;
}
.punctuation.definition.comment {
color: #65737e;
}
.punctuation.definition.string {
color: #c0c5ce;
}
.punctuation.definition.variable {
color: #c0c5ce;
}
.punctuation.definition.string {
color: #c0c5ce;
}
.punctuation.definition.parameters {
color: #c0c5ce;
}
.punctuation.definition.string {
color: #c0c5ce;
}
.punctuation.definition.array {
color: #c0c5ce;
}
.none {
color: #c0c5ce;
}
.keyword.operator {
color: #c0c5ce;
}
.keyword {
color: #b48ead;
}
.variable {
color: #bf616a;
}
.variable.other.dollar.only.js {
color: #bf616a;
}
.entity.name.function {
color: #8fa1b3;
}
.meta.require {
color: #8fa1b3;
}
.support.function.any-method {
color: #8fa1b3;
}
.variable.function {
color: #8fa1b3;
}
.support.class {
color: #ebcb8b;
}
.entity.name.class {
color: #ebcb8b;
}
.entity.name.type.class {
color: #ebcb8b;
}
.meta.class {
color: #eff1f5;
}
.keyword.other.special-method {
color: #8fa1b3;
}
.storage {
color: #b48ead;
}
.support.function {
color: #96b5b4;
}
.string {
color: #a3be8c;
}
.constant.other.symbol {
color: #a3be8c;
}
.entity.other.inherited-class {
color: #a3be8c;
}
.constant.numeric {
color: #d08770;
}
.none {
color: #d08770;
}
.none {
color: #d08770;
}
.constant {
color: #d08770;
}
.entity.name.tag {
color: #bf616a;
}
.entity.other.attribute-name {
color: #d08770;
}
.entity.other.attribute-name.id {
color: #8fa1b3;
}
.punctuation.definition.entity {
color: #8fa1b3;
}
.meta.selector {
color: #b48ead;
}
.none {
color: #d08770;
}
.markup.heading {
color: #8fa1b3;
}
.punctuation.definition.heading {
color: #8fa1b3;
}
.entity.name.section {
color: #8fa1b3;
}
.keyword.other.unit {
color: #d08770;
}
.markup.bold {
color: #ebcb8b;
font-weight: 700;
}
.punctuation.definition.bold {
color: #ebcb8b;
font-weight: 700;
}
.markup.italic {
color: #b48ead;
font-style: italic;
}
.punctuation.definition.italic {
color: #b48ead;
font-style: italic;
}
.markup.raw.inline {
color: #a3be8c;
}
.string.other.link {
color: #bf616a;
}
.meta.link {
color: #d08770;
}
.meta.image {
color: #d08770;
}
.markup.list {
color: #bf616a;
}
.markup.quote {
color: #d08770;
}
.meta.separator {
color: #c0c5ce;
background-color: #4f5b66;
}
.markup.inserted {
color: #a3be8c;
}
.markup.inserted.git_gutter {
color: #a3be8c;
}
.markup.deleted {
color: #bf616a;
}
.markup.deleted.git_gutter {
color: #bf616a;
}
.markup.changed {
color: #b48ead;
}
.markup.changed.git_gutter {
color: #b48ead;
}
.markup.ignored {
color: #4f5b66;
}
.markup.ignored.git_gutter {
color: #4f5b66;
}
.markup.untracked {
color: #4f5b66;
}
.markup.untracked.git_gutter {
color: #4f5b66;
}
.constant.other.color {
color: #96b5b4;
}
.string.regexp {
color: #96b5b4;
}
.constant.character.escape {
color: #96b5b4;
}
.punctuation.section.embedded {
color: #ab7967;
}
.variable.interpolation {
color: #ab7967;
}
.invalid.illegal {
color: #2b303b;
background-color: #bf616a;
}
.markup.deleted.git_gutter {
color: #f92672;
}
.markup.inserted.git_gutter {
color: #a6e22e;
}
.markup.changed.git_gutter {
color: #967efb;
}
.markup.ignored.git_gutter {
color: #565656;
}
.markup.untracked.git_gutter {
color: #565656;
}
.source .block {
display: inline;
}
span .block {
display: inline;
}

View File

@ -1,179 +0,0 @@
.markdown p {
font-family: sans-serif;
font-weight: 400;
}
.markdown p > * {
font-family: sans-serif;
font-weight: 400;
}
.markdown h1,
h2,
h3 {
margin-top: 20px;
margin-bottom: 10px;
}
.markdown h1 {
font-size: 2em;
@apply text-amber;
}
.markdown h2 {
font-size: 1.5em;
@apply text-amber;
}
.markdown h3 {
font-size: 1.17em;
@apply text-amber;
}
.markdown h4 {
font-size: 1em;
@apply text-amber;
margin-bottom: 10px;
}
.markdown h5 {
font-size: 0.83em;
@apply text-amber;
margin-bottom: 10px;
}
.markdown h6 {
font-size: 0.75em;
@apply text-amber;
margin-bottom: 10px;
}
.markdown blockquote {
padding: 0 1em;
margin: 0 0 20px;
font-size: 17.5px;
border-left: 4px solid #8b949e;
}
.markdown img {
max-width: 35%;
vertical-align: middle;
display: inline;
}
.markdown table {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
margin-top: 10px;
}
tbody > tr:nth-child(odd) > td,
.table-striped > tbody > tr:nth-child(odd) > th {
background-color: #1b1b1b;
}
.table-striped > tbody > tr:nth-child(odd) > td,
.table-striped > tbody > tr:nth-child(odd) > th {
background-color: #1b1b1b;
}
table > thead > tr > th,
table > tbody > tr > th,
table > tfoot > tr > th,
table > thead > tr > td,
table > tbody > tr > td,
table > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border: 1px solid #3b434b;
}
.markdown hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #3b434b;
}
.markdown {
/* border: 1px solid #3b434b; */
border-radius: 6px;
padding-right: 32px;
padding-left: 32px;
}
.markdown pre {
padding: 12px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #1b1b1b;
border-radius: 6px;
margin: 10px 0;
}
.markdown code pre tt {
font-family: "JetbrainsMono", SFMono-Regular, Consolas, Liberation Mono, Menlo,
monospace;
}
.markdown blockquote > :last-child {
margin-bottom: 0;
}
.markdown blockquote,
.markdown p,
.markdown ol,
.markdown pre,
.markdown table,
.markdown ul {
margin-top: 0;
margin-bottom: 16px;
}
h1,
h2 {
padding-bottom: 0.3em;
border-bottom: 1px solid #3b434b;
}
.markdown li {
display: list-item;
text-align: -webkit-match-parent;
font-family: sans-serif;
font-weight: 400;
}
.markdown ol {
list-style-type: decimal;
}
.markdown ol,
ul {
padding: 0;
margin-left: 40px;
}
.markdown strong {
font-family: sans-serif;
}
.markdown code.inline {
font-family: "JetBrains Mono", monospace;
background-color: rgb(240 246 252 / 15%);
font-weight: bold;
border-radius: 4px;
font-size: 80%;
margin: 0;
padding: 0.1em 0.4em;
}
.markdown ul {
list-style: disc;
}
.markdown ol {
list-style: decimal;
}
.markdown ul, ol {
padding: 0;
margin-left: 40px;
}

View File

@ -1,15 +0,0 @@
// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import "../css/app.css";
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import deps with the dep name or local files with a relative path, for example:
//
// import {Socket} from "phoenix"
// import socket from "./socket"
//
import "phoenix_html";

View File

@ -1,63 +0,0 @@
// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "assets/js/app.js".
// To use Phoenix channels, the first step is to import Socket,
// and connect at the socket path in "lib/web/endpoint.ex".
//
// Pass the token on params as below. Or remove it
// from the params if you are not using authentication.
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "lib/web/router.ex":
//
// pipeline :browser do
// ...
// plug MyAuth
// plug :put_user_token
// end
//
// defp put_user_token(conn, _) do
// if current_user = conn.assigns[:current_user] do
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
// assign(conn, :user_token, token)
// else
// conn
// end
// end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "lib/web/templates/layout/app.html.eex":
//
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
//
// You will need to verify the user token in the "connect/3" function
// in "lib/web/channels/user_socket.ex":
//
// def connect(%{"token" => token}, socket, _connect_info) do
// # max_age: 1209600 is equivalent to two weeks in seconds
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
// {:ok, user_id} ->
// {:ok, assign(socket, :user, user_id)}
// {:error, reason} ->
// :error
// end
// end
//
// Finally, connect to the socket:
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("topic:subtopic", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket

3399
assets/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
{
"repository": {},
"description": " ",
"license": "MIT",
"scripts": {
"deploy": "NODE_ENV=production node build.js",
"watch": "node build.js"
},
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html"
},
"devDependencies": {
"autoprefixer": "^10.3.1",
"esbuild": "^0.12.22",
"esbuild-plugin-postcss2": "0.0.9",
"fs-extra": "^10.0.0",
"postcss": "^8.3.6",
"postcss-import": "^15.0.0",
"tailwindcss": "^3.4.3"
}
}

View File

@ -1,7 +0,0 @@
module.exports = {
plugins: {
"postcss-import": {},
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,5 +0,0 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /

View File

@ -1,22 +0,0 @@
module.exports = {
content: [
"../lib/**/*.ex",
"../lib/**/*.leex",
"../lib/**/*.eex",
"../lib/**/*.heex",
"./js/**/*.js",
],
darkMode: 'media', // or 'media' or 'class'
theme: {
extend: {
colors: {
amber: "#ff9800",
"light-grey": "#212121",
},
},
},
variants: {
extend: {},
},
plugins: [],
};

View File

@ -1,31 +0,0 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
# General application configuration
import Config
config :ketbin,
ecto_repos: [Ketbin.Repo]
# Configures the endpoint
config :ketbin, KetbinWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "9uGXQJhm7f2+kO7HKKWUmgBpaRK6oMt795GQKFNlnxdhUjG0Xpy/XddOb03H1aH2",
render_errors: [view: KetbinWeb.ErrorView, accepts: ~w(html json), layout: false],
pubsub_server: Ketbin.PubSub,
live_view: [signing_salt: "76BWv42A"]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View File

@ -1,67 +0,0 @@
import Config
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
config :ketbin, KetbinWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [
node: [
"build.js",
cd: Path.expand("../assets", __DIR__)
]
]
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
# mix phx.gen.cert
#
# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
#
# https: [
# port: 4001,
# cipher_suite: :strong,
# keyfile: "priv/cert/selfsigned_key.pem",
# certfile: "priv/cert/selfsigned.pem"
# ],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
# Watch static and templates for browser reloading.
config :ketbin, KetbinWeb.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/ketbin_web/(live|views)/.*(ex)$",
~r"lib/ketbin_web/templates/.*(eex)$"
]
]
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
# Import the secrets config
import_config "dev.secret.exs"

View File

@ -1,24 +0,0 @@
import Mix.Config
# Configure your database
config :ketbin, Ketbin.Repo,
username: "postgres",
password: "postgres",
database: "ketbin_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
smtp_relay = "<Your SMTP Relay Here>"
username = "<Your SMTP username here>"
password = "<Your SMTP password here>"
# configure mailer
config :ketbin, Ketbin.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: smtp_relay,
username: username,
password: password,
tls: :always,
auth: :always,
port: 587

View File

@ -1,55 +0,0 @@
import Config
# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :ketbin, KetbinWeb.Endpoint,
url: [host: "katb.in", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production
config :logger, level: :info
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
# config :ketbin, KetbinWeb.Endpoint,
# ...
# url: [host: "example.com", port: 443],
# https: [
# port: 443,
# cipher_suite: :strong,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
# transport_options: [socket_opts: [:inet6]]
# ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your endpoint, ensuring
# no data is ever sent via http, always redirecting to https:
#
# config :ketbin, KetbinWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# Finally import the config/prod.secret.exs which loads secrets
# and configuration from environment variables.
# import_config "prod.secret.exs"

View File

@ -1,71 +0,0 @@
# In this file, we load production configuration and secrets
# from environment variables. You can also hardcode secrets,
# although such is generally not recommended and you have to
# remember to add this file to your .gitignore.
import Config
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
config :ketbin, Ketbin.Repo,
ssl: false,
# verify: :verify_peer,
socket_options: [:inet6],
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
# cacertfile: "priv/cert.pem"
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
config :ketbin, KetbinWeb.Endpoint,
http: [
port: String.to_integer(System.get_env("PORT") || "4000"),
transport_options: [socket_opts: [:inet6]]
],
secret_key_base: secret_key_base,
server: true
smtp_relay =
System.get_env("SWOOSH_SMTP_RELAY") ||
raise """
environment variable SWOOSH_SMTP_RELAY is missing.
"""
username =
System.get_env("SWOOSH_USERNAME") ||
raise """
environment variable SWOOSH_USERNAME is missing.
"""
password =
System.get_env("SWOOSH_PASSWORD") ||
raise """
environment variable SWOOSH_PASSWORD is missing.
"""
# configure mailer
config :ketbin, Ketbin.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: smtp_relay,
username: username,
password: password,
tls: :always,
auth: :always,
port: 587
# ## Using releases (Elixir v1.9+)
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start each relevant endpoint:
#
#
# Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information.

View File

@ -1,25 +0,0 @@
import Config
# Only in tests, remove the complexity from the password hashing algorithm
config :bcrypt_elixir, :log_rounds, 1
# Configure your database
#
# The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information.
config :ketbin, Ketbin.Repo,
username: "postgres",
password: "postgres",
database: "ketbin_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :ketbin, KetbinWeb.Endpoint,
http: [port: 4002],
server: false
# Print only warnings and errors during test
config :logger, level: :warn

View File

@ -0,0 +1,29 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: front-service
spec:
replicas: 1
selector:
matchLabels:
app: front-service
template:
metadata:
labels:
app: front-service
spec:
nodeSelector:
"beta.kubernetes.io/os": linux
containers:
- name: front-service
image: atechnohazard/katbin-web
ports:
- containerPort: 5000
name: front-service
resources:
limits:
cpu: 500m
requests:
cpu: 200m
imagePullPolicy: Always

51
deployment/deployment.yml Normal file
View File

@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: katbin-service
spec:
replicas: 1
selector:
matchLabels:
app: katbin-service
template:
metadata:
labels:
app: katbin-service
spec:
nodeSelector:
"beta.kubernetes.io/os": linux
containers:
- name: katbin-service
image: atechnohazard/katbin
env:
- name: KATBIN_POOL_SIZE
value: '5'
- name: JIRACHI_POOL_SIZE
value: '5'
- name: ROCKET_PORT
value: '5000'
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: katbin-secrets
key: DATABASE_URL
- name: JIRACHI_DB_URL
valueFrom:
secretKeyRef:
name: katbin-secrets
key: DATABASE_URL
- name: ROCKET_SECRET_KEY
valueFrom:
secretKeyRef:
name: katbin-secrets
key: ROCKET_SECRET_KEY
ports:
- containerPort: 5000
name: katbin-service
resources:
limits:
cpu: 500m
requests:
cpu: 200m
imagePullPolicy: Always

34
deployment/ingress.yml Normal file
View File

@ -0,0 +1,34 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: katbin-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- api.katb.in
- katb.in
secretName: katbin-tls
rules:
- host: api.katb.in
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: katbin-service
port:
number: 80
- host: katb.in
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: front-service
port:
number: 81

View File

@ -0,0 +1,31 @@
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/do-loadbalancer-enable-proxy-protocol: 'true'
service.beta.kubernetes.io/do-loadbalancer-hostname: "workaround.katb.in"
labels:
helm.sh/chart: ingress-nginx-3.23.0
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/version: 0.44.0
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/component: controller
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
type: LoadBalancer
externalTrafficPolicy: Local
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/component: controller

View File

@ -0,0 +1,9 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: katbin-service-podbudget
spec:
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: katbin-service

View File

@ -0,0 +1,19 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
# You must replace this email address with your own.
# Let's Encrypt will use this to contact you about expiring
# certificates, and issues related to your account.
email: amolele@gmail.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource that will be used to store the account's private key.
name: letsencrypt-prod
# Add a single challenge solver, HTTP01 using nginx
solvers:
- http01:
ingress:
class: nginx

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: Service
metadata:
name: front-service
spec:
ports:
- port: 81
targetPort: 5000
selector:
app: front-service

10
deployment/service.yml Normal file
View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: Service
metadata:
name: katbin-service
spec:
ports:
- port: 80
targetPort: 5000
selector:
app: katbin-service

View File

@ -0,0 +1,19 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
# You must replace this email address with your own.
# Let's Encrypt will use this to contact you about expiring
# certificates, and issues related to your account.
email: amolele@gmail.com
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource that will be used to store the account's private key.
name: letsencrypt-staging
# Add a single challenge solver, HTTP01 using nginx
solvers:
- http01:
ingress:
class: nginx

5
diesel.toml Normal file
View File

@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see postgres/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

View File

@ -1,45 +0,0 @@
# fly.toml file generated for katbin on 2022-05-06T17:46:21+05:30
app = "katbin"
kill_signal = "SIGTERM"
kill_timeout = 5
processes = []
# [deploy]
# release_command = "/app/bin/migrate"
[env]
PHX_HOST = "katbin.fly.dev"
PORT = "8080"
[experimental]
allowed_public_ports = []
auto_rollback = true
[[services]]
http_checks = []
internal_port = 8080
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 10000
soft_limit = 6000
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"

View File

@ -1,9 +0,0 @@
defmodule Ketbin do
@moduledoc """
Ketbin keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end

View File

@ -1,349 +0,0 @@
defmodule Ketbin.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.Query, warn: false
alias Ketbin.Repo
alias Ketbin.Accounts.{User, UserToken, UserNotifier}
## Database getters
@doc """
Gets a user by email.
## Examples
iex> get_user_by_email("foo@example.com")
%User{}
iex> get_user_by_email("unknown@example.com")
nil
"""
def get_user_by_email(email) when is_binary(email) do
Repo.get_by(User, email: email)
end
@doc """
Gets a user by email and password.
## Examples
iex> get_user_by_email_and_password("foo@example.com", "correct_password")
%User{}
iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
nil
"""
def get_user_by_email_and_password(email, password)
when is_binary(email) and is_binary(password) do
user = Repo.get_by(User, email: email)
if User.valid_password?(user, password), do: user
end
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.get!(User, id)
## User registration
@doc """
Registers a user.
## Examples
iex> register_user(%{field: value})
{:ok, %User{}}
iex> register_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user_registration(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user_registration(%User{} = user, attrs \\ %{}) do
User.registration_changeset(user, attrs, hash_password: false)
end
## Settings
@doc """
Returns an `%Ecto.Changeset{}` for changing the user email.
## Examples
iex> change_user_email(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user_email(user, attrs \\ %{}) do
User.email_changeset(user, attrs)
end
@doc """
Emulates that the email will change without actually changing
it in the database.
## Examples
iex> apply_user_email(user, "valid password", %{email: ...})
{:ok, %User{}}
iex> apply_user_email(user, "invalid password", %{email: ...})
{:error, %Ecto.Changeset{}}
"""
def apply_user_email(user, password, attrs) do
user
|> User.email_changeset(attrs)
|> User.validate_current_password(password)
|> Ecto.Changeset.apply_action(:update)
end
@doc """
Updates the user email using the given token.
If the token matches, the user email is updated and the token is deleted.
The confirmed_at date is also updated to the current time.
"""
def update_user_email(user, token) do
context = "change:#{user.email}"
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
%UserToken{sent_to: email} <- Repo.one(query),
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
:ok
else
_ -> :error
end
end
defp user_email_multi(user, email, context) do
changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset()
Ecto.Multi.new()
|> Ecto.Multi.update(:user, changeset)
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context]))
end
@doc """
Delivers the update email instructions to the given user.
## Examples
iex> deliver_update_email_instructions(user, current_email, &Routes.user_update_email_url(conn, :edit, &1))
{:ok, %{to: ..., body: ...}}
"""
def deliver_update_email_instructions(%User{} = user, current_email, update_email_url_fun)
when is_function(update_email_url_fun, 1) do
{encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
Repo.insert!(user_token)
UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
end
@doc """
Returns an `%Ecto.Changeset{}` for changing the user password.
## Examples
iex> change_user_password(user)
%Ecto.Changeset{data: %User{}}
"""
def change_user_password(user, attrs \\ %{}) do
User.password_changeset(user, attrs, hash_password: false)
end
@doc """
Updates the user password.
## Examples
iex> update_user_password(user, "valid password", %{password: ...})
{:ok, %User{}}
iex> update_user_password(user, "invalid password", %{password: ...})
{:error, %Ecto.Changeset{}}
"""
def update_user_password(user, password, attrs) do
changeset =
user
|> User.password_changeset(attrs)
|> User.validate_current_password(password)
Ecto.Multi.new()
|> Ecto.Multi.update(:user, changeset)
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
|> Repo.transaction()
|> case do
{:ok, %{user: user}} -> {:ok, user}
{:error, :user, changeset, _} -> {:error, changeset}
end
end
## Session
@doc """
Generates a session token.
"""
def generate_user_session_token(user) do
{token, user_token} = UserToken.build_session_token(user)
Repo.insert!(user_token)
token
end
@doc """
Gets the user with the given signed token.
"""
def get_user_by_session_token(token) do
{:ok, query} = UserToken.verify_session_token_query(token)
Repo.one(query)
end
@doc """
Deletes the signed token with the given context.
"""
def delete_session_token(token) do
Repo.delete_all(UserToken.token_and_context_query(token, "session"))
:ok
end
## Confirmation
@doc """
Delivers the confirmation email instructions to the given user.
## Examples
iex> deliver_user_confirmation_instructions(user, &Routes.user_confirmation_url(conn, :confirm, &1))
{:ok, %{to: ..., body: ...}}
iex> deliver_user_confirmation_instructions(confirmed_user, &Routes.user_confirmation_url(conn, :confirm, &1))
{:error, :already_confirmed}
"""
def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun)
when is_function(confirmation_url_fun, 1) do
if user.confirmed_at do
{:error, :already_confirmed}
else
{encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
Repo.insert!(user_token)
UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
end
end
@doc """
Confirms a user by the given token.
If the token matches, the user account is marked as confirmed
and the token is deleted.
"""
def confirm_user(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
%User{} = user <- Repo.one(query),
{:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
{:ok, user}
else
_ -> :error
end
end
defp confirm_user_multi(user) do
Ecto.Multi.new()
|> Ecto.Multi.update(:user, User.confirm_changeset(user))
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"]))
end
## Reset password
@doc """
Delivers the reset password email to the given user.
## Examples
iex> deliver_user_reset_password_instructions(user, &Routes.user_reset_password_url(conn, :edit, &1))
{:ok, %{to: ..., body: ...}}
"""
def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun)
when is_function(reset_password_url_fun, 1) do
{encoded_token, user_token} = UserToken.build_email_token(user, "reset_password")
Repo.insert!(user_token)
UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
end
@doc """
Gets the user by reset password token.
## Examples
iex> get_user_by_reset_password_token("validtoken")
%User{}
iex> get_user_by_reset_password_token("invalidtoken")
nil
"""
def get_user_by_reset_password_token(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
%User{} = user <- Repo.one(query) do
user
else
_ -> nil
end
end
@doc """
Resets the user password.
## Examples
iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"})
{:ok, %User{}}
iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
{:error, %Ecto.Changeset{}}
"""
def reset_user_password(user, attrs) do
Ecto.Multi.new()
|> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
|> Repo.transaction()
|> case do
{:ok, %{user: user}} -> {:ok, user}
{:error, :user, changeset, _} -> {:error, changeset}
end
end
end

View File

@ -1,139 +0,0 @@
defmodule Ketbin.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
@derive {Inspect, except: [:password]}
schema "users" do
field :email, :string
field :password, :string, virtual: true
field :hashed_password, :string
field :confirmed_at, :naive_datetime
timestamps()
end
@doc """
A user changeset for registration.
It is important to validate the length of both email and password.
Otherwise databases may truncate the email without warnings, which
could lead to unpredictable or insecure behaviour. Long passwords may
also be very expensive to hash for certain algorithms.
## Options
* `:hash_password` - Hashes the password so it can be stored securely
in the database and ensures the password field is cleared to prevent
leaks in the logs. If password hashing is not needed and clearing the
password field is not desired (like when using this changeset for
validations on a LiveView form), this option can be set to `false`.
Defaults to `true`.
"""
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :password])
|> validate_email()
|> validate_password(opts)
end
defp validate_email(changeset) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> unsafe_validate_unique(:email, Ketbin.Repo)
|> unique_constraint(:email)
end
defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 8, max: 80)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts)
end
defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)
if hash_password? && password && changeset.valid? do
changeset
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
@doc """
A user changeset for changing the email.
It requires the email to change otherwise an error is added.
"""
def email_changeset(user, attrs) do
user
|> cast(attrs, [:email])
|> validate_email()
|> case do
%{changes: %{email: _}} = changeset -> changeset
%{} = changeset -> add_error(changeset, :email, "did not change")
end
end
@doc """
A user changeset for changing the password.
## Options
* `:hash_password` - Hashes the password so it can be stored securely
in the database and ensures the password field is cleared to prevent
leaks in the logs. If password hashing is not needed and clearing the
password field is not desired (like when using this changeset for
validations on a LiveView form), this option can be set to `false`.
Defaults to `true`.
"""
def password_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:password])
|> validate_confirmation(:password, message: "does not match password")
|> validate_password(opts)
end
@doc """
Confirms the account by setting `confirmed_at`.
"""
def confirm_changeset(user) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
change(user, confirmed_at: now)
end
@doc """
Verifies the password.
If there is no user or the user doesn't have a password, we call
`Bcrypt.no_user_verify/0` to avoid timing attacks.
"""
def valid_password?(%Ketbin.Accounts.User{hashed_password: hashed_password}, password)
when is_binary(hashed_password) and byte_size(password) > 0 do
Bcrypt.verify_pass(password, hashed_password)
end
def valid_password?(_, _) do
Bcrypt.no_user_verify()
false
end
@doc """
Validates the current password otherwise adds an error to the changeset.
"""
def validate_current_password(changeset, password) do
if valid_password?(changeset.data, password) do
changeset
else
add_error(changeset, :current_password, "is not valid")
end
end
end

View File

@ -1,96 +0,0 @@
defmodule Ketbin.Accounts.UserNotifier do
import Swoosh.Email
# For simplicity, this module simply logs messages to the terminal.
# You should replace it by a proper email or notification tool, such as:
#
# * Swoosh - https://hexdocs.pm/swoosh
# * Bamboo - https://hexdocs.pm/bamboo
#
defp deliver(to, body, subject) do
require Logger
Logger.debug(body)
Task.start(fn ->
new()
|> to(to)
|> from({"Katbin", "noreply@katb.in"})
|> subject(subject)
|> text_body(body)
|> Ketbin.Mailer.deliver()
end)
{:ok, %{to: to, body: body}}
end
@doc """
Deliver instructions to confirm account.
"""
def deliver_confirmation_instructions(user, url) do
deliver(
user.email,
"""
==============================
Hi #{user.email},
You can confirm your account by visiting the URL below:
#{url}
If you didn't create an account with us, please ignore this.
==============================
""",
"Account confirmation"
)
end
@doc """
Deliver instructions to reset a user password.
"""
def deliver_reset_password_instructions(user, url) do
deliver(
user.email,
"""
==============================
Hi #{user.email},
You can reset your password by visiting the URL below:
#{url}
If you didn't request this change, please ignore this.
==============================
""",
"Password reset requested"
)
end
@doc """
Deliver instructions to update a user email.
"""
def deliver_update_email_instructions(user, url) do
deliver(
user.email,
"""
==============================
Hi #{user.email},
You can change your email by visiting the URL below:
#{url}
If you didn't request this change, please ignore this.
==============================
""",
"Email update requested"
)
end
end

View File

@ -1,139 +0,0 @@
defmodule Ketbin.Accounts.UserToken do
use Ecto.Schema
import Ecto.Query
@hash_algorithm :sha256
@rand_size 32
# It is very important to keep the reset password token expiry short,
# since someone with access to the email may take over the account.
@reset_password_validity_in_days 1
@confirm_validity_in_days 7
@change_email_validity_in_days 7
@session_validity_in_days 60
schema "users_tokens" do
field :token, :binary
field :context, :string
field :sent_to, :string
belongs_to :user, Ketbin.Accounts.User
timestamps(updated_at: false)
end
@doc """
Generates a token that will be stored in a signed place,
such as session or cookie. As they are signed, those
tokens do not need to be hashed.
"""
def build_session_token(user) do
token = :crypto.strong_rand_bytes(@rand_size)
{token, %Ketbin.Accounts.UserToken{token: token, context: "session", user_id: user.id}}
end
@doc """
Checks if the token is valid and returns its underlying lookup query.
The query returns the user found by the token.
"""
def verify_session_token_query(token) do
query =
from token in token_and_context_query(token, "session"),
join: user in assoc(token, :user),
where: token.inserted_at > ago(@session_validity_in_days, "day"),
select: user
{:ok, query}
end
@doc """
Builds a token with a hashed counter part.
The non-hashed token is sent to the user email while the
hashed part is stored in the database, to avoid reconstruction.
The token is valid for a week as long as users don't change
their email.
"""
def build_email_token(user, context) do
build_hashed_token(user, context, user.email)
end
defp build_hashed_token(user, context, sent_to) do
token = :crypto.strong_rand_bytes(@rand_size)
hashed_token = :crypto.hash(@hash_algorithm, token)
{Base.url_encode64(token, padding: false),
%Ketbin.Accounts.UserToken{
token: hashed_token,
context: context,
sent_to: sent_to,
user_id: user.id
}}
end
@doc """
Checks if the token is valid and returns its underlying lookup query.
The query returns the user found by the token.
"""
def verify_email_token_query(token, context) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
days = days_for_context(context)
query =
from token in token_and_context_query(hashed_token, context),
join: user in assoc(token, :user),
where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email,
select: user
{:ok, query}
:error ->
:error
end
end
defp days_for_context("confirm"), do: @confirm_validity_in_days
defp days_for_context("reset_password"), do: @reset_password_validity_in_days
@doc """
Checks if the token is valid and returns its underlying lookup query.
The query returns the user token record.
"""
def verify_change_email_token_query(token, context) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
query =
from token in token_and_context_query(hashed_token, context),
where: token.inserted_at > ago(@change_email_validity_in_days, "day")
{:ok, query}
:error ->
:error
end
end
@doc """
Returns the given token with the given context.
"""
def token_and_context_query(token, context) do
from Ketbin.Accounts.UserToken, where: [token: ^token, context: ^context]
end
@doc """
Gets all tokens for the given user for the given contexts.
"""
def user_and_contexts_query(user, :all) do
from t in Ketbin.Accounts.UserToken, where: t.user_id == ^user.id
end
def user_and_contexts_query(user, [_ | _] = contexts) do
from t in Ketbin.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts
end
end

View File

@ -1,34 +0,0 @@
defmodule Ketbin.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
def start(_type, _args) do
children = [
# Start the Ecto repository
Ketbin.Repo,
# Start the Telemetry supervisor
KetbinWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Ketbin.PubSub},
# Start the Endpoint (http/https)
KetbinWeb.Endpoint
# Start a worker by calling: Ketbin.Worker.start_link(arg)
# {Ketbin.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Ketbin.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
KetbinWeb.Endpoint.config_change(changed, removed)
:ok
end
end

View File

@ -1,3 +0,0 @@
defmodule Ketbin.Mailer do
use Swoosh.Mailer, otp_app: :ketbin
end

View File

@ -1,108 +0,0 @@
defmodule Ketbin.Pastes do
@moduledoc """
The Pastes context.
"""
import Ecto.Query, warn: false
alias Ketbin.Repo
alias Ketbin.Pastes.Paste
@doc """
Returns the list of pastes.
## Examples
iex> list_pastes()
[%Paste{}, ...]
"""
def list_pastes do
Repo.all(Paste)
end
def list_pastes_by_user(user_id) do
Repo.all(from(p in Paste, where: p.belongs_to == ^user_id))
end
@doc """
Gets a single paste.
Raises `Ecto.NoResultsError` if the Paste does not exist.
## Examples
iex> get_paste!(123)
%Paste{}
iex> get_paste!(456)
** (Ecto.NoResultsError)
"""
def get_paste!(id), do: Repo.get!(Paste, id)
@doc """
Creates a paste.
## Examples
iex> create_paste(%{field: value})
{:ok, %Paste{}}
iex> create_paste(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_paste(attrs \\ %{}) do
%Paste{}
|> Paste.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a paste.
## Examples
iex> update_paste(paste, %{field: new_value})
{:ok, %Paste{}}
iex> update_paste(paste, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_paste(%Paste{} = paste, attrs) do
paste
|> Paste.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a paste.
## Examples
iex> delete_paste(paste)
{:ok, %Paste{}}
iex> delete_paste(paste)
{:error, %Ecto.Changeset{}}
"""
def delete_paste(%Paste{} = paste) do
Repo.delete(paste)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking paste changes.
## Examples
iex> change_paste(paste)
%Ecto.Changeset{data: %Paste{}}
"""
def change_paste(%Paste{} = paste, attrs \\ %{}) do
Paste.changeset(paste, attrs)
end
end

View File

@ -1,23 +0,0 @@
defmodule Ketbin.Pastes.Paste do
use Ecto.Schema
import Ecto.Changeset
@derive {Jason.Encoder, only: [:content, :is_url, :belongs_to]}
@primary_key {:id, :string, autogenerate: false}
@derive {Phoenix.Param, key: :id}
schema "pastes" do
field :content, :string
field :is_url, :boolean, default: false
field :belongs_to, :id
timestamps()
end
@doc false
def changeset(paste, attrs) do
paste
|> cast(attrs, [:is_url, :content, :id, :belongs_to])
|> validate_required([:is_url, :content])
|> unique_constraint(:id, name: :pastes_pkey)
end
end

View File

@ -1,33 +0,0 @@
defmodule Ketbin.Pastes.Utils do
defp rand_vowel do
String.graphemes("aeiou")
|> Enum.take_random(1)
|> Enum.at(0)
end
defp rand_consonant do
String.graphemes("bcdfghjklmnpqrstvwxyz")
|> Enum.take_random(1)
|> Enum.at(0)
end
def generate_key(length \\ 10) do
random = Enum.random([0, 1])
Enum.map(0..length, fn i ->
if Integer.mod(i, 2) == random, do: rand_consonant(), else: rand_vowel()
end)
|> List.to_string()
end
def is_url?(url) do
try do
uri = URI.parse(url)
uri.scheme != nil && uri.host =~ "." &&
Enum.member?(["https", "http", "mailto"], uri.scheme) && !(uri.host =~ "katb.in")
rescue
FunctionClauseError -> false
end
end
end

View File

@ -1,5 +0,0 @@
defmodule Ketbin.Repo do
use Ecto.Repo,
otp_app: :ketbin,
adapter: Ecto.Adapters.Postgres
end

View File

@ -1,10 +0,0 @@
defmodule Ketbin.Utils.Syntax do
use Rustler, otp_app: :ketbin, crate: "ketbin_utils_syntax"
# When your NIF is loaded, it will override this function.
def add(_a, _b), do: error()
def highlight_text(_text, _lang), do: error()
defp error(), do: :erlang.nif_error(:nif_not_loaded)
end

View File

@ -1,81 +0,0 @@
defmodule KetbinWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
This can be used in your application as:
use KetbinWeb, :controller
use KetbinWeb, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define any helper function in modules
and import those modules here.
"""
def controller do
quote do
use Phoenix.Controller, namespace: KetbinWeb
import Plug.Conn
import KetbinWeb.Gettext
alias KetbinWeb.Router.Helpers, as: Routes
end
end
def view do
quote do
use Phoenix.View,
root: "lib/ketbin_web/templates",
namespace: KetbinWeb
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
# Include shared imports and aliases for views
unquote(view_helpers())
end
end
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
end
end
def channel do
quote do
use Phoenix.Channel
import KetbinWeb.Gettext
end
end
defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import KetbinWeb.ErrorHelpers
import KetbinWeb.Gettext
alias KetbinWeb.Router.Helpers, as: Routes
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

View File

@ -1,35 +0,0 @@
defmodule KetbinWeb.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", KetbinWeb.RoomChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# KetbinWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(_socket), do: nil
end

View File

@ -1,45 +0,0 @@
defmodule KetbinWeb.Api.PasteController do
use KetbinWeb, :controller
alias Ketbin.Pastes
alias Ketbin.Pastes.Utils
def show(conn, %{"id" => id}) do
[head | _tail] = String.split(id, ".")
# fetch paste from db
paste = Pastes.get_paste!(head)
render(conn, "paste.json", paste: paste)
end
def create(%{assigns: %{current_user: current_user}} = conn, %{"paste" => paste_params}) do
# generate phonetic key
id = Utils.generate_key()
# check if content is a url
is_url =
Map.get(paste_params, "content")
|> Utils.is_url?()
# put id and is_url values into changeset
paste_params =
Map.put(paste_params, "id", id)
|> Map.put("is_url", is_url)
|> Map.put("belongs_to", current_user && current_user.id)
# attempt to create a paste
case Pastes.create_paste(paste_params) do
# all good
{:ok, paste} ->
conn
|> put_status(:created)
|> render("paste.json", paste: paste)
# something went wrong, bail
{:error, %Ecto.Changeset{} = _changeset} ->
conn
|> put_status(:internal_server_error)
|> render("error.json")
end
end
end

View File

@ -1,142 +0,0 @@
defmodule KetbinWeb.PageController do
require Logger
use KetbinWeb, :controller
alias Ketbin.Pastes
alias Ketbin.Pastes.Paste
alias Ketbin.Pastes.Utils
def index(conn, _params) do
changeset = Pastes.change_paste(%Paste{})
render(conn, "index.html", changeset: changeset)
end
def show(%{assigns: %{show_edit: show_edit}} = conn, %{"id" => id}) do
[head | tail] = String.split(id, ".")
# fetch paste from db
paste =
try do
Pastes.get_paste!(head)
rescue
Ecto.NoResultsError ->
Pastes.get_paste!(id)
end
# paste is a url, redirect
# regular paste, show content
if paste.is_url do
redirect(conn,
external: paste.content |> String.replace("\r", "") |> String.replace("\n", "")
)
else
render(conn, "show.html",
paste: paste,
show_edit: show_edit,
extension: List.first(tail) || "",
page_title: paste.id
)
end
end
def showlink(%{assigns: %{show_edit: show_edit}} = conn, %{"id" => id}) do
[head | tail] = String.split(id, ".")
paste = Pastes.get_paste!(head)
render(conn, "shorten.html",
paste: paste,
show_edit: show_edit,
extension: if(tail == [], do: "", else: tail),
page_title: paste.id
)
end
def raw(conn, %{"id" => id}) do
paste = Pastes.get_paste!(id)
text(conn, paste.content)
end
def create(%{assigns: %{current_user: current_user}} = conn, %{"paste" => paste_params}) do
# if custom url exists, use it as id, else generate phonetic id
id =
case Map.get(paste_params, "custom_url", "") do
custom_url when custom_url != "" -> custom_url
_ -> Utils.generate_key()
end
# check if content is a url
is_url =
Map.get(paste_params, "content")
|> Utils.is_url?()
# put id and is_url values into changeset
paste_params =
Map.put(paste_params, "id", id)
|> Map.put("is_url", is_url)
|> Map.put("belongs_to", current_user && current_user.id)
# attempt to create a paste
case Pastes.create_paste(paste_params) do
# all good, redirect
{:ok, paste} ->
unless is_url do
conn
# is a regular paste, take to regular route
|> redirect(to: Routes.page_path(conn, :show, paste))
else
conn
# is a url, take to route with /v/ prefix
|> redirect(to: Routes.page_path(conn, :showlink, paste))
end
# something went wrong, bail
{:error, %Ecto.Changeset{} = changeset} ->
case changeset.errors[:id] do
{"has already been taken", _} ->
render(conn, "index.html", changeset: changeset, already_taken: true)
_ ->
render(conn, "index.html", changeset: changeset)
end
end
end
def edit(conn, %{"id" => id}) do
paste = Pastes.get_paste!(id)
changeset = Pastes.change_paste(paste)
render(conn, "edit.html", paste: paste, changeset: changeset)
end
def update(conn, %{"id" => id, "paste" => paste_params}) do
paste = Pastes.get_paste!(id)
# check if content is a url
is_url =
Map.get(paste_params, "content")
|> Utils.is_url?()
paste_params = Map.put(paste_params, "is_url", is_url)
case Pastes.update_paste(paste, paste_params) do
{:ok, paste} ->
unless is_url do
conn
|> put_flash(:info, "Paste updated successfully.")
|> redirect(to: Routes.page_path(conn, :show, paste))
else
conn
|> put_flash(:info, "Paste updated successfully.")
|> redirect(to: Routes.page_path(conn, :showlink, paste))
end
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", paste: paste, changeset: changeset)
end
end
def pastes(%{assigns: %{current_user: current_user}} = conn, _params) do
pastes = Pastes.list_pastes_by_user(current_user.id)
render(conn, "pastes.html", pastes: pastes)
end
end

View File

@ -1,179 +0,0 @@
defmodule KetbinWeb.UserAuth do
import Plug.Conn
import Phoenix.Controller
alias Ketbin.Accounts
alias Ketbin.Pastes
alias KetbinWeb.Router.Helpers, as: Routes
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
# the token expiry itself in UserToken.
@max_age 60 * 60 * 24 * 60
@remember_me_cookie "_ketbin_web_user_remember_me"
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
@doc """
Logs the user in.
It renews the session ID and clears the whole session
to avoid fixation attacks. See the renew_session
function to customize this behaviour.
It also sets a `:live_socket_id` key in the session,
so LiveView sessions are identified and automatically
disconnected on log out. The line can be safely removed
if you are not using LiveView.
"""
def log_in_user(conn, user, params \\ %{}) do
token = Accounts.generate_user_session_token(user)
user_return_to = get_session(conn, :user_return_to)
conn
|> renew_session()
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|> maybe_write_remember_me_cookie(token, params)
|> put_flash(:info, "Logged in successfully!")
|> redirect(to: user_return_to || signed_in_path(conn))
end
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end
defp maybe_write_remember_me_cookie(conn, _token, _params) do
conn
end
# This function renews the session ID and erases the whole
# session to avoid fixation attacks. If there is any data
# in the session you may want to preserve after log in/log out,
# you must explicitly fetch the session data before clearing
# and then immediately set it after clearing, for example:
#
# defp renew_session(conn) do
# preferred_locale = get_session(conn, :preferred_locale)
#
# conn
# |> configure_session(renew: true)
# |> clear_session()
# |> put_session(:preferred_locale, preferred_locale)
# end
#
defp renew_session(conn) do
conn
|> configure_session(renew: true)
|> clear_session()
end
@doc """
Logs the user out.
It clears all session data for safety. See renew_session.
"""
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
KetbinWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
end
@doc """
Authenticates the user by looking into the session
and remember me token.
"""
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user = user_token && Accounts.get_user_by_session_token(user_token)
assign(conn, :current_user, user)
end
def owns_paste(%{params: %{"id" => id}, assigns: %{current_user: user}} = conn, _params) do
[head | _tail] = String.split(id, ".")
paste =
try do
Pastes.get_paste!(head)
rescue
Ecto.NoResultsError ->
Pastes.get_paste!(id)
end
assign(conn, :show_edit, (user && user.id == paste.belongs_to) || false)
end
def ensure_owns_paste(%{params: %{"id" => id}, assigns: %{current_user: user}} = conn, _params) do
[head | _tail] = String.split(id, ".")
paste = Pastes.get_paste!(head)
allow_edit = (user && user.id == paste.belongs_to) || false
unless allow_edit do
conn
|> put_flash(:error, "You don't own this paste!")
|> redirect(to: Routes.page_path(conn, :show, id))
else
conn
end
end
defp ensure_user_token(conn) do
if user_token = get_session(conn, :user_token) do
{user_token, conn}
else
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
if user_token = conn.cookies[@remember_me_cookie] do
{user_token, put_session(conn, :user_token, user_token)}
else
{nil, conn}
end
end
end
@doc """
Used for routes that require the user to not be authenticated.
"""
def redirect_if_user_is_authenticated(conn, _opts) do
if conn.assigns[:current_user] do
conn
|> redirect(to: signed_in_path(conn))
|> halt()
else
conn
end
end
@doc """
Used for routes that require the user to be authenticated.
If you want to enforce the user email is confirmed before
they use the application at all, here would be a good place.
"""
def require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new))
|> halt()
end
end
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: "/"
end

View File

@ -1,53 +0,0 @@
defmodule KetbinWeb.UserConfirmationController do
use KetbinWeb, :controller
alias Ketbin.Accounts
def new(conn, _params) do
render(conn, "new.html")
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
end
# Regardless of the outcome, show an impartial success/error message.
conn
|> put_flash(
:info,
"If your email is in our system and it has not been confirmed yet, " <>
"you will receive an email with instructions shortly."
)
|> redirect(to: "/")
end
# Do not log in the user after confirmation to avoid a
# leaked token giving the user access to the account.
def confirm(conn, %{"token" => token}) do
case Accounts.confirm_user(token) do
{:ok, _} ->
conn
|> put_flash(:info, "User confirmed successfully.")
|> redirect(to: "/")
:error ->
# If there is a current user and the account was already confirmed,
# then odds are that the confirmation link was already visited, either
# by some automation or by the user themselves, so we redirect without
# a warning message.
case conn.assigns do
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
redirect(conn, to: "/")
%{} ->
conn
|> put_flash(:error, "User confirmation link is invalid or it has expired.")
|> redirect(to: "/")
end
end
end
end

View File

@ -1,31 +0,0 @@
defmodule KetbinWeb.UserRegistrationController do
use KetbinWeb, :controller
alias Ketbin.Accounts
alias Ketbin.Accounts.User
alias KetbinWeb.UserAuth
def new(conn, _params) do
changeset = Accounts.change_user_registration(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Accounts.register_user(user_params) do
{:ok, user} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
conn
|> put_flash(:info, "User created successfully.")
|> UserAuth.log_in_user(user)
|> redirect(to: "/users/confirm")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
end

View File

@ -1,59 +0,0 @@
defmodule KetbinWeb.UserResetPasswordController do
use KetbinWeb, :controller
alias Ketbin.Accounts
plug :get_user_by_reset_password_token when action in [:edit, :update]
def new(conn, _params) do
render(conn, "new.html")
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_reset_password_instructions(
user,
&Routes.user_reset_password_url(conn, :edit, &1)
)
end
# Regardless of the outcome, show an impartial success/error message.
conn
|> put_flash(
:info,
"If your email is in our system, you will receive instructions to reset your password shortly."
)
|> redirect(to: "/")
end
def edit(conn, _params) do
render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user))
end
# Do not log in the user after reset password to avoid a
# leaked token giving the user access to the account.
def update(conn, %{"user" => user_params}) do
case Accounts.reset_user_password(conn.assigns.user, user_params) do
{:ok, _} ->
conn
|> put_flash(:info, "Password reset successfully.")
|> redirect(to: Routes.user_session_path(conn, :new))
{:error, changeset} ->
render(conn, "edit.html", changeset: changeset)
end
end
defp get_user_by_reset_password_token(conn, _opts) do
%{"token" => token} = conn.params
if user = Accounts.get_user_by_reset_password_token(token) do
conn |> assign(:user, user) |> assign(:token, token)
else
conn
|> put_flash(:error, "Reset password link is invalid or it has expired.")
|> redirect(to: "/")
|> halt()
end
end
end

View File

@ -1,28 +0,0 @@
defmodule KetbinWeb.UserSessionController do
use KetbinWeb, :controller
alias Ketbin.Accounts
alias KetbinWeb.UserAuth
def new(conn, _params) do
render(conn, "new.html", error_message: nil)
end
def create(conn, %{"user" => user_params}) do
%{"email" => email, "password" => password} = user_params
if user = Accounts.get_user_by_email_and_password(email, password) do
UserAuth.log_in_user(conn, user, user_params)
else
render(conn, "new.html", error_message: "Invalid email or password")
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
conn
end
end

View File

@ -1,76 +0,0 @@
defmodule KetbinWeb.UserSettingsController do
use KetbinWeb, :controller
alias Ketbin.Accounts
alias KetbinWeb.UserAuth
plug :assign_email_and_password_changesets
def edit(conn, _params) do
render(conn, "edit.html")
end
def update(conn, %{"action" => "update_email"} = params) do
%{"current_password" => password, "user" => user_params} = params
user = conn.assigns.current_user
case Accounts.apply_user_email(user, password, user_params) do
{:ok, applied_user} ->
Accounts.deliver_update_email_instructions(
applied_user,
user.email,
&Routes.user_settings_url(conn, :confirm_email, &1)
)
conn
|> put_flash(
:info,
"A link to confirm your email change has been sent to the new address."
)
|> redirect(to: Routes.user_settings_path(conn, :edit))
{:error, changeset} ->
render(conn, "edit.html", email_changeset: changeset)
end
end
def update(conn, %{"action" => "update_password"} = params) do
%{"current_password" => password, "user" => user_params} = params
user = conn.assigns.current_user
case Accounts.update_user_password(user, password, user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "Password updated successfully.")
|> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
|> UserAuth.log_in_user(user)
conn
{:error, changeset} ->
render(conn, "edit.html", password_changeset: changeset)
end
end
def confirm_email(conn, %{"token" => token}) do
case Accounts.update_user_email(conn.assigns.current_user, token) do
:ok ->
conn
|> put_flash(:info, "Email changed successfully.")
|> redirect(to: Routes.user_settings_path(conn, :edit))
:error ->
conn
|> put_flash(:error, "Email change link is invalid or it has expired.")
|> redirect(to: Routes.user_settings_path(conn, :edit))
end
end
defp assign_email_and_password_changesets(conn, _opts) do
user = conn.assigns.current_user
conn
|> assign(:email_changeset, Accounts.change_user_email(user))
|> assign(:password_changeset, Accounts.change_user_password(user))
end
end

View File

@ -1,56 +0,0 @@
defmodule KetbinWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :ketbin
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_ketbin_key",
signing_salt: "HmwUQdXk"
]
socket "/socket", KetbinWeb.UserSocket,
websocket: true,
longpoll: false
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :ketbin,
gzip: false,
only: ~w(assets fonts images favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :ketbin
end
plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
length: 10_000_000,
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug CORSPlug
plug KetbinWeb.Router
end

View File

@ -1,24 +0,0 @@
defmodule KetbinWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
import KetbinWeb.Gettext
# Simple translation
gettext("Here is the string to translate")
# Plural translation
ngettext("Here is the string to translate",
"Here are the strings to translate",
3)
# Domain-based translation
dgettext("errors", "Here is the error message to translate")
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :ketbin
end

View File

@ -1,115 +0,0 @@
defmodule KetbinWeb.Router do
use KetbinWeb, :router
import KetbinWeb.UserAuth
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
end
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
plug :fetch_current_user
end
# scope to ensure user is authenticated
scope "/", KetbinWeb do
pipe_through [:browser, :require_authenticated_user]
get "/pastes", PageController, :pastes
end
scope "/", KetbinWeb do
pipe_through :browser
get "/", PageController, :index
get "/:id/raw", PageController, :raw
post "/", PageController, :create
end
# scope to check if user is owner of paste
scope "/", KetbinWeb do
pipe_through [:browser, :owns_paste]
get "/:id", PageController, :show
get "/v/:id", PageController, :showlink
end
# scope to ensure user is owner of paste
scope "/", KetbinWeb do
pipe_through [:browser, :ensure_owns_paste]
get "/edit/:id", PageController, :edit
patch "/:id", PageController, :update
put "/:id", PageController, :update
end
scope "/api", KetbinWeb.Api, as: :api do
pipe_through :api
resources "/paste", PasteController, only: [:show, :index, :create]
end
# Other scopes may use custom stacks.
# scope "/api", KetbinWeb do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
live_dashboard "/dashboard", metrics: KetbinWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
## Authentication routes
scope "/", KetbinWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
get "/users/log_in", UserSessionController, :new
post "/users/log_in", UserSessionController, :create
get "/users/reset_password", UserResetPasswordController, :new
post "/users/reset_password", UserResetPasswordController, :create
get "/users/reset_password/:token", UserResetPasswordController, :edit
put "/users/reset_password/:token", UserResetPasswordController, :update
end
scope "/", KetbinWeb do
pipe_through [:browser, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
end
scope "/", KetbinWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
end
end

View File

@ -1,55 +0,0 @@
defmodule KetbinWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
# Telemetry poller will execute the given period measurements
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
# Database Metrics
summary("ketbin.repo.query.total_time", unit: {:native, :millisecond}),
summary("ketbin.repo.query.decode_time", unit: {:native, :millisecond}),
summary("ketbin.repo.query.query_time", unit: {:native, :millisecond}),
summary("ketbin.repo.query.queue_time", unit: {:native, :millisecond}),
summary("ketbin.repo.query.idle_time", unit: {:native, :millisecond}),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {KetbinWeb, :count_users, []}
]
end
end

View File

@ -1,30 +0,0 @@
<script>
function toggleDropdown() {
const dropdown = document.querySelector('#dropdown');
const dropdownLabel = document.querySelector('#dropdown-label');
if (dropdown.classList.contains('hidden')) {
dropdown.classList.remove('hidden');
dropdown.classList.add('absolute');
dropdownLabel.innerHTML = dropdownLabel.innerHTML.replace('▼', '▲');
} else {
dropdown.classList.add('hidden');
dropdown.classList.remove('absolute');
dropdownLabel.innerHTML = dropdownLabel.innerHTML.replace('▲', '▼');
}
}
</script>
<ul>
<%= if @current_user do %>
<div class="relative">
<li id="dropdown-label" onclick="toggleDropdown()" class="text-[#ff9800] cursor-pointer select-none"><%= @current_user.email %> ▼</li>
<div id="dropdown" class="hidden right-0 z-50 bg-[#313131] px-4 py-2">
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
<li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
<li><%= link "My Pastes", to: Routes.page_path(@conn, :pastes) %></li>
</div>
</div>
<% else %>
<li><%= link "Register", to: Routes.user_registration_path(@conn, :new) %></li>
<li><%= link "Log in", to: Routes.user_session_path(@conn, :new) %></li>
<% end %>
</ul>

View File

@ -1,100 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= if Map.has_key?(assigns, :page_title) do %>
<title>Katbin - <%= @page_title %></title>
<% else %>
<title>Katbin</title>
<% end %>
<title itemprop="name">Katbin</title>
<meta property="og:type" content="object">
<meta property="og:title" content="Katbin">
<meta property="og:image" content="/favicon.ico">
<meta name="twitter:title" content="Katbin">
<meta itemprop="name" content="Katbin">
<meta name="application-name" content="Katbin">
<meta property="og:site_name" content="Katbin">
<meta name="theme-color" content="#1a1a1a">
<meta property="og:locale" content="en">
<meta name="language" content="en">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@Spherical_Kat">
<meta name="twitter:image" content="/favicon.ico">
<meta name="robots" content="index,follow">
<meta name="googlebot" content="index,follow">
<meta name="keywords" content="paste,pastebin,url,urlshortener,url-shortener,code,programming,bin,cat,kat,paste,share,save,login">
<meta name="coverage" content="Worldwide">
<meta name="distribution" content="Global">
<meta name="HandheldFriendly" content="True">
<meta name="HandheldFriendly" content="True">
<meta name="apple-mobile-web-app-title" content="Katbin">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-touch-fullscreen" content="yes">
<%= if assigns[:paste] do %>
<meta name="description" content={@paste.content}>
<meta itemprop="description" content={@paste.content}>
<meta property="og:description" content={@paste.content}>
<meta name="twitter:description" content={@paste.content}>
<meta name="url" content={"https://katb.in/#{@paste.id}"}>
<meta name="twitter:url" content={"https://katb.in/#{@paste.id}"}>
<meta property="og:url" content={"https://katb.in/#{@paste.id}"}>
<% else %>
<meta name="description" content="Paste, save, share! A pastebin and URL shortener for all your needs.">
<meta property="og:description" content="Paste, save, share! A pastebin and URL shortener for all your needs.">
<meta itemprop="og:description" content="Paste, save, share! A pastebin and URL shortener for all your needs.">
<meta name="twitter:description" content="Paste, save, share! A pastebin and URL shortener for all your needs.">
<meta name="url" content="https://katb.in">
<meta name="twitter:url" content="https://katb.in">
<meta property="og:url" content="https://katb.in">
<% end %>
<link rel="stylesheet" href={ Routes.static_path(@conn, "/assets/app.css") }/>
<script defer type="text/javascript" src={ Routes.static_path(@conn, "/assets/app.js") }></script>
</head>
<body class="flex flex-col ">
<header class="flex w-full justify-between items-center py-3 px-6">
<a href={ Routes.page_path(@conn, :index) }>
<span class="font-semibold text-xl tracking-tight">
<span class="text-amber text-xl">&lt;Kat</span>bin/&gt;
</span>
</a>
<nav role="navigation">
<ul>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li>
<%# <%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %>
</li>
<% end %>
</ul>
<%= render "_user_menu.html", assigns %>
</nav>
</header>
<main class="flex flex-col w-full h-full max-h-full overflow-hidden bg-light-grey" role="main">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= @inner_content %>
</main>
<footer class="font-bold">
<div class="flex px-4 py-1 text-xs sm:text-base justify-between text-amber" style="background: #1a1a1a; font-family: JetbrainsMono">
<a href="https://kat.bio">
© <%= DateTime.utc_now().year %> SphericalKat
</a>
<a href="https://github.com/sphericalkat/katbin">
Fork me!
</a>
</div>
</footer>
</body>
</html>

View File

@ -1 +0,0 @@
<%= render "form.html", Map.put(assigns, :action, Routes.page_path(@conn, :update, @paste)) |> Map.put(:is_edit, true) %>

View File

@ -1,40 +0,0 @@
<%= form_for @changeset, @action, [class: "flex flex-col w-full h-full relative", id: "page_form"], fn f -> %>
<script>
document.addEventListener("keydown", function(e) {
if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83) {
e.preventDefault();
document.getElementById("page_form").submit()
}
}, false);
</script>
<%= if @changeset.action do %>
<div class="w-full text-center bg-amber">
<p>Oops, something went wrong!</p>
</div>
<% end %>
<%= if assigns[:already_taken] do %>
<div class="w-full text-center bg-amber">
<p>This custom URL has already been taken.</p>
</div>
<% end %>
<div class="w-full h-full">
<%= textarea f, :content, [class: "w-full h-full px-6 py-4 outline-none bg-light-grey font-bold resize-none", placeholder: "> Paste, save, share! (Pasting just a URL will shorten it!)"] %>
<div class="flex absolute top-0 right-0 p-4">
<%= if @current_user && !assigns[:is_edit] do %>
<div>
<%= text_input f, :custom_url, [class: "px-2 mr-2 outline-none text-black px-2 py-1", placeholder: "Custom URL"] %>
</div>
<% end %>
<button type="submit">
<svg
class="h-6 w-6 cursor-pointer fill-current text-white hover:text-amber"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path d="M17.6 3.6c-.4-.4-.9-.6-1.4-.6H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V7.8c0-.5-.2-1-.6-1.4l-2.8-2.8zM12 19c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3zm1-10H7c-1.1 0-2-.9-2-2s.9-2 2-2h6c1.1 0 2 .9 2 2s-.9 2-2 2z"/>
</svg>
</button>
</div>
</div>
<% end %>

View File

@ -1 +0,0 @@
<%= render "form.html", Map.put(assigns, :action, Routes.page_path(@conn, :create)) %>

View File

@ -1,11 +0,0 @@
<div class="flex relative flex-col w-full h-full">
<ul class="break-word py-4 h-full w-full overflow-y-auto">
<%= for paste <- @pastes do %>
<li class="flex flex-row items-center justify-between">
<a href={ Routes.page_path(@conn, :show, paste) } class="">
https://katb.in/v/<%= paste.id %>
</a>
</li>
<% end %>
</ul>
</div>

View File

@ -1,16 +0,0 @@
<div class="flex relative flex-col w-full h-full">
<div class="flex absolute top-0 right-0 p-4">
<%= if @show_edit do%>
<a href={ Routes.page_path(@conn, :edit, @paste.id) } class="text-white hover:text-amber">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 cursor-pointer fill-current">
<path d="M3 17.46v3.04c0 .28.22.5.5.5h3.04c.13 0 .26-.05.35-.15L17.81 9.94l-3.75-3.75L3.15 17.1c-.1.1-.15.22-.15.36zM20.71 7.04a.996.996 0 0 0 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"></path>
</svg>
</a>
<% end %>
</div>
<div class="break-word px-6 py-4 h-full w-full overflow-y-auto">
<span>
Your shortened url is: <a href={Routes.page_path(@conn, :show, @paste.id) }>https://katb.in/<%= @paste.id %></a>
</span>
</div>
</div>

View File

@ -1,16 +0,0 @@
<div class="flex relative flex-col w-full h-full">
<div class="flex absolute top-0 right-0 p-4">
<%= if @show_edit do%>
<a href={ Routes.page_path(@conn, :edit, @paste.id) } class="text-white hover:text-amber">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 cursor-pointer fill-current">
<path d="M3 17.46v3.04c0 .28.22.5.5.5h3.04c.13 0 .26-.05.35-.15L17.81 9.94l-3.75-3.75L3.15 17.1c-.1.1-.15.22-.15.36zM20.71 7.04a.996.996 0 0 0 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"></path>
</svg>
</a>
<% end %>
</div>
<%= if @extension == "md" do%>
<div class="break-word px-6 py-4 h-full w-full markdown overflow-y-auto"><%= raw Earmark.as_html!(@paste.content, escape: true, gfm_tables: true) |> HtmlSanitizeEx.markdown_html %></div>
<% else %>
<code class="break-word px-6 py-4 h-full w-full overflow-y-auto"><%= raw Ketbin.Utils.Syntax.highlight_text(@paste.content, @extension) |> HtmlSanitizeEx.html5 %></code>
<% end %>
</div>

View File

@ -1,19 +0,0 @@
<div class="flex flex-col w-full h-full justify-center items-center">
<h1 class="font-bold text-4xl text-amber pt-4">Resend confirmation instructions</h1>
<%= form_for :user, Routes.user_confirmation_path(@conn, :create), [class: "flex flex-col h-full justify-center items-start m-auto"], fn f -> %>
<div class="flex flex-col w-full">
<%= label f, :email %>
<%= email_input f, :email, [class: "text-black px-2 py-1 outline-none", required: true] %>
</div>
<div class="bg-amber mt-4 rounded-sm px-2 py-1">
<%= submit "Resend confirmation instructions" %>
</div>
<% end %>
<p class="mb-4 text-amber">
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>
</div>

View File

@ -1,32 +0,0 @@
<div class="flex flex-col w-full h-full justify-center items-center">
<h1 class="font-bold text-4xl text-amber pt-4">Register</h1>
<%= form_for @changeset, Routes.user_registration_path(@conn, :create), [class: "flex flex-col h-full justify-center items-start m-auto"], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="flex flex-col w-full">
<%= label f, :email %>
<%= email_input f, :email, [class: "text-black px-2 py-1 outline-none", required: true] %>
<%= error_tag f, :email %>
</div>
<div class="flex flex-col mt-2 w-full">
<%= label f, :password, [class: ""] %>
<%= password_input f, :password, [class: "text-black px-2 py-1 outline-none", required: true] %>
<%= error_tag f, :password %>
</div>
<div class="bg-amber mt-4 rounded-sm px-2 py-1">
<%= submit "Sign up" %>
</div>
<% end %>
<p class="mb-4 text-amber">
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %> <span>|</span>
<%= link "Forgot password?", to: Routes.user_reset_password_path(@conn, :new) %>
</p>
</div>

View File

@ -1,32 +0,0 @@
<div class="flex flex-col w-full h-full justify-center items-center">
<h1 class="font-bold text-4xl text-amber pt-4">Reset password</h1>
<%= form_for @changeset, Routes.user_reset_password_path(@conn, :update, @token), [class: "flex flex-col h-full justify-center items-start m-auto"], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="flex flex-col w-full">
<%= label f, :password, "New password" %>
<%= password_input f, :password, [class: "text-black px-2 py-1 outline-none", required: true] %>
<%= error_tag f, :password %>
</div>
<div class="flex flex-col w-full">
<%= label f, :password_confirmation, "Confirm new password" %>
<%= password_input f, :password_confirmation, [class: "text-black px-2 py-1 outline-none", required: true] %>
<%= error_tag f, :password_confirmation %>
</div>
<div class="bg-amber mt-4 rounded-sm px-2 py-1">
<%= submit "Reset password" %>
</div>
<% end %>
<p class="mb-4 text-amber">
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>
</div>

View File

@ -1,19 +0,0 @@
<div class="flex flex-col w-full h-full justify-center items-center">
<h1 class="font-bold text-4xl text-amber pt-4">Forgot your password?</h1>
<%= form_for :user, Routes.user_reset_password_path(@conn, :create), [class: "flex flex-col h-full justify-center items-start m-auto"], fn f -> %>
<div class="flex flex-col w-full">
<%= label f, :email %>
<%= email_input f, :email, [class: "text-black px-2 py-1 outline-none", required: true] %>
</div>
<div class="bg-amber mt-4 rounded-sm px-2 py-1">
<%= submit "Send instructions to reset password" %>
</div>
<% end %>
<p class="mb-4 text-amber">
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>
</div>

View File

@ -1,34 +0,0 @@
<div class="flex flex-col w-full h-full justify-center items-center">
<h1 class="font-bold text-4xl text-amber pt-4">Log in</h1>
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "flex flex-col h-full justify-center items-start m-auto"], fn f -> %>
<%= if @error_message do %>
<div class="alert alert-danger">
<p><%= @error_message %></p>
</div>
<% end %>
<div class="flex flex-col w-full">
<%= label f, :email %>
<%= email_input f, :email, [class: "text-black px-2 py-1 outline-none", required: true] %>
</div>
<div class="flex flex-col mt-2 w-full">
<%= label f, :password %>
<%= password_input f, :password, [class: "text-black px-2 py-1 outline-none", required: true] %>
</div>
<div class="flex mt-2 w-full justify-center items-center">
<%= checkbox f, :remember_me, [class: "mr-2 outline-none"] %>
<%= label f, :remember_me, "Keep me logged in for 60 days" %>
</div>
<div class="bg-amber mt-4 rounded-sm px-2 py-1">
<%= submit "Log in" %>
</div>
<% end %>
<p class="mb-4 text-amber">
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
</p>
</div>

View File

@ -1,66 +0,0 @@
<h1 class="font-bold text-4xl text-amber pt-4 w-full text-center">Settings</h1>
<div class="flex w-full h-full justify-center items-center">
<%= form_for @email_changeset, Routes.user_settings_path(@conn, :update), [class: "flex flex-col h-full justify-center items-start m-auto"], fn f -> %>
<h3 class="font-bold text-2xl text-amber pb-4">Change email</h3>
<%= if @email_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= hidden_input f, :action, name: "action", value: "update_email" %>
<div class="flex flex-col w-full">
<%= label f, :email %>
<%= email_input f, :email, [class: "text-black px-2 py-1 outline-none", required: true] %>
<%= error_tag f, :email %>
</div>
<div class="flex flex-col w-full">
<%= label f, :current_password, for: "current_password_for_email" %>
<%= password_input f, :current_password, [class: "text-black px-2 py-1 outline-none", required: true, name: "current_password", id: "current_password_for_email"]%>
<%= error_tag f, :current_password %>
</div>
<div class="bg-amber mt-4 rounded-sm px-2 py-1">
<%= submit "Change email" %>
</div>
<% end %>
<%= form_for @password_changeset, Routes.user_settings_path(@conn, :update), [class: "flex flex-col h-full justify-center items-start m-auto"], fn f -> %>
<h3 class="font-bold text-2xl text-amber pb-4">Change password</h3>
<%= if @password_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= hidden_input f, :action, name: "action", value: "update_password" %>
<div class="flex flex-col w-full">
<%= label f, :password, "New password" %>
<%= password_input f, :password, [class: "text-black px-2 py-1 outline-none", required: true] %>
<%= error_tag f, :password %>
</div>
<div class="flex flex-col w-full">
<%= label f, :password_confirmation, "Confirm new password" %>
<%= password_input f, :password_confirmation, [class: "text-black px-2 py-1 outline-none", required: true] %>
<%= error_tag f, :password_confirmation %>
</div>
<div class="flex flex-col w-full">
<%= label f, :current_password, for: "current_password_for_password" %>
<%= password_input f, :current_password, [class: "text-black px-2 py-1 outline-none", required: true, name: "current_password", id: "current_password_for_password"] %>
<%= error_tag f, :current_password %>
</div>
<div class="bg-amber mt-4 rounded-sm px-2 py-1">
<%= submit "Change password" %>
</div>
<% end %>
</div>

View File

@ -1,18 +0,0 @@
defmodule KetbinWeb.Api.PasteView do
use KetbinWeb, :view
def render("paste.json", %{paste: paste}) do
%{
id: paste.id,
content: paste.content,
is_url: paste.is_url
}
end
def render("error.json", _assigns) do
%{
success: false,
msg: "Something went wrong"
}
end
end

View File

@ -1,47 +0,0 @@
defmodule KetbinWeb.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
class: "absolute top-0 left-0 px-6 py-4",
phx_feedback_for: input_name(form, field)
)
end)
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate "is invalid" in the "errors" domain
# dgettext("errors", "is invalid")
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
# This requires us to call the Gettext module passing our gettext
# backend as first argument.
#
# Note we use the "errors" domain, which means translations
# should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do
Gettext.dngettext(KetbinWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(KetbinWeb.Gettext, "errors", msg, opts)
end
end
end

View File

@ -1,16 +0,0 @@
defmodule KetbinWeb.ErrorView do
use KetbinWeb, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
# def render("500.html", _assigns) do
# "Internal Server Error"
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.html" becomes
# "Not Found".
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View File

@ -1,3 +0,0 @@
defmodule KetbinWeb.LayoutView do
use KetbinWeb, :view
end

View File

@ -1,3 +0,0 @@
defmodule KetbinWeb.PageView do
use KetbinWeb, :view
end

View File

@ -1,3 +0,0 @@
defmodule KetbinWeb.UserConfirmationView do
use KetbinWeb, :view
end

View File

@ -1,3 +0,0 @@
defmodule KetbinWeb.UserRegistrationView do
use KetbinWeb, :view
end

View File

@ -1,3 +0,0 @@
defmodule KetbinWeb.UserResetPasswordView do
use KetbinWeb, :view
end

View File

@ -1,3 +0,0 @@
defmodule KetbinWeb.UserSessionView do
use KetbinWeb, :view
end

View File

@ -1,3 +0,0 @@
defmodule KetbinWeb.UserSettingsView do
use KetbinWeb, :view
end

View File

@ -1,32 +0,0 @@
defmodule Ketbin.Release do
@moduledoc """
This module is used by the entrypoint to migrate the database
"""
@app :ketbin
require Logger
def migrate do
Application.ensure_all_started(@app)
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.load(@app)
end
end

0
migrations/.gitkeep Normal file
View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS users

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS users
(
id VARCHAR PRIMARY KEY,
username VARCHAR,
password VARCHAR,
activated BOOLEAN
)

Some files were not shown because too many files have changed in this diff Show More