Compare commits

...

2 commits

Author SHA1 Message Date
Joe Ardent
a31b41cb9e doesn't not work 2024-02-25 16:34:32 -08:00
Joe Ardent
ff673840e3 goodbye elixir 2024-02-18 12:50:27 -08:00
22 changed files with 1563 additions and 141 deletions

View file

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

27
.gitignore vendored
View file

@ -1,26 +1 @@
# 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 third-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").
queen-*.tar
# Temporary files, for example, from tests.
/tmp/
/target

4
.rustfmt.toml Normal file
View file

@ -0,0 +1,4 @@
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
wrap_comments = true
edition = "2021"

1223
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "queenie"
version = "0.1.0"
edition = "2021"
[dependencies]
askama = { version = "0.12", default-features = false, features = ["with-axum", "serde"] }
askama_axum = { version = "0.4.0", default-features = false }
axum = { version = "0.7", default-features = false, features = ["tokio", "http1", "form"] }
justerror = { version = "1" }
serde = { version = "1", default-features = false, features = ["derive"] }
thiserror = { version = "1" }
tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] }
tower-sessions = { version = "0.10", default-features = false, features = ["axum-core", "memory-store"] }
unicode-segmentation = { version = "1", default-features = false }

View file

@ -1 +0,0 @@
meow

1
assets/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

40
assets/ww.css Normal file
View file

@ -0,0 +1,40 @@
body {
background-color: darkgrey
}
table {
border-collapse: collapse;
width: 100%;
}
th {
background-color: ghostwhite;
}
th, td {
text-align: left;
padding: 8px;
}
tr:nth-child(even) {background-color: ghostwhite;}
#header {
text-align: end;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.warning {
background-color: goldenrod;
}
.watchtitle {
font-style: italic;
}
.header_logged_in {
display: flex;
flex-direction: column;
}

View file

@ -1,3 +0,0 @@
import Config
config(:queen, :secret_key_base, System.fetch_env!("QUEEN_COOKIE_SECRET"))

View file

@ -1,27 +0,0 @@
defmodule QueenRouter do
use Plug.Router
plug(Plug.Logger)
plug(:match)
plug(:dispatch)
plug(Plug.Session, store: :cookie, key: "_queen_session", signing_salt: "J6PHP10BHF23")
plug(:fetch_session)
plug(Plug.CSRFProtection)
get "/signup" do
fetch_session(conn) |> put_session(:verify, "meow") |> send_resp(200, "signup")
end
get "/success/:payment" do
conn = fetch_session(conn)
verify = get_session(conn, :verify)
:logger.info("got verify: #{verify}")
:logger.info("got payment receipt code #{payment}")
send_resp(conn, 200, "huzzah")
end
match _ do
send_resp(conn, 404, "you lost, kitty-cat?")
end
end

View file

@ -1,21 +0,0 @@
defmodule Queen.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
{Bandit, plug: QueenRouter}
# Starts a worker by calling: Queen.Worker.start_link(arg)
# {Queen.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Queen.Supervisor]
Supervisor.start_link(children, opts)
end
end

32
mix.exs
View file

@ -1,32 +0,0 @@
defmodule Queen.MixProject do
use Mix.Project
def project do
[
app: :queen,
version: "0.1.0",
elixir: "~> 1.16.1",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
# extra_applications: [:logger, :ecto_sqlite3, :ecto, :bandit, :plug]
extra_applications: [:logger],
mod: {Queen.Application, []}
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:bandit, "~> 1.2"},
{:plug, "~> 1.5"},
{:ecto_sqlite3, "~> 0.15"},
{:ecto, "~> 3.11"}
]
end
end

View file

@ -1,18 +0,0 @@
%{
"bandit": {:hex, :bandit, "1.2.1", "aa485b4ac175065b8e0fb5864ddd5dd7b50d52336b36f61c82f484c3718b3d15", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27393e590a407f1b7d51c5fee4737f139fe224a30449ce25061eac70f763896b"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"},
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
"exqlite": {:hex, :exqlite, "0.19.0", "0f3ee29e35bed38552dd0ed59600aa81c78f867f5b5ff0e17d330148e0465483", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "55a8fbb0443f03d4a256e3458bd1203eff5037a6624b76460eaaa9080f462b06"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"thousand_island": {:hex, :thousand_island, "1.3.2", "bc27f9afba6e1a676dd36507d42e429935a142cf5ee69b8e3f90bff1383943cd", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e085b93012cd1057b378fce40cbfbf381ff6d957a382bfdd5eca1a98eec2535"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
}

156
src/main.rs Normal file
View file

@ -0,0 +1,156 @@
use std::{
fmt::{Debug, Display},
net::SocketAddr,
};
use askama::Template;
use axum::{
extract::{Form, Path},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Router,
};
use serde::{Deserialize, Serialize};
use tower_sessions::{MemoryStore, Session, SessionManagerLayer};
#[macro_use]
extern crate justerror;
const SIGNUP_KEY: &str = "meow";
#[derive(Default, Deserialize, Serialize)]
struct Counter(usize);
/// Displays the signup form.
async fn get_signup() -> impl IntoResponse {
SignupPage::default()
}
/// Receives the form with the user signup fields filled out.
async fn post_signup(session: Session, Form(form): Form<SignupForm>) -> impl IntoResponse {
todo!()
}
/// Called from Stripe with the receipt of payment.
async fn signup_success(session: Session, receipt: Option<Path<String>>) -> impl IntoResponse {
todo!()
}
#[tokio::main]
async fn main() {
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store).with_secure(false);
let app = Router::new()
//.nest_service("/assets", assets_svc)
.route("/signup", get(get_signup).post(post_signup))
.route("/signup_success/:receipt", get(signup_success))
.layer(session_layer);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[Error(desc = "Could not create user.")]
#[non_exhaustive]
pub struct CreateUserError(#[from] CreateUserErrorKind);
impl IntoResponse for CreateUserError {
fn into_response(self) -> Response {
(StatusCode::FORBIDDEN, format!("{:?}", self.0)).into_response()
}
}
#[Error]
#[non_exhaustive]
pub enum CreateUserErrorKind {
AlreadyExists,
#[error(desc = "Usernames must be between 1 and 20 characters long")]
BadUsername,
PasswordMismatch,
#[error(desc = "Password must have at least 4 and at most 50 characters")]
BadPassword,
#[error(desc = "Display name must be less than 100 characters long")]
BadDisplayname,
BadEmail,
BadPayment,
}
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
pub struct SignupForm {
pub username: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub displayname: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub email: Option<String>,
pub password: String,
pub pw_verify: String,
pub invitation: String,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct User {
pub username: String,
pub displayname: Option<String>,
pub email: Option<String>,
pub password: String,
pub pw_verify: String,
}
impl Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pw_check = if self.password == self.pw_verify {
"password matched"
} else {
"PASSWORD MISMATCH"
};
f.debug_struct("User")
.field("username", &self.username)
.field("displayname", &self.displayname)
.field("email", &self.email)
.field("pw-check", &pw_check)
.finish()
}
}
impl Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let uname = &self.username;
let dname = if let Some(ref n) = self.displayname {
n
} else {
""
};
let email = if let Some(ref e) = self.email { e } else { "" };
write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}")
}
}
pub(crate) fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let opt = <Option<String> as serde::Deserialize>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => std::str::FromStr::from_str(s)
.map_err(serde::de::Error::custom)
.map(Some),
}
}
#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)]
#[template(path = "signup.html")]
pub struct SignupPage {
pub username: String,
pub displayname: Option<String>,
pub email: Option<String>,
pub password: String,
pub pw_verify: String,
}

29
templates/base.html Normal file
View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ title }} - What 2 Watch{% endblock %}</title>
<link rel="stylesheet" href="/assets/ww.css">
{% block head %}{% endblock %}
</head>
<body>
<div id="header">
{% block header %}{% endblock %}
</div>
<div id="content">
<hr />
{% block content %}{% endblock %}
</div>
<div id="footer">
{% block footer %}{% endblock %}
</div>
<script src="/assets/htmx.min.js"></script>
</body>
</html>

26
templates/index.html Normal file
View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Welcome to What 2 Watch, Bish{% endblock %}
{% block content %}
<h1>Welcome to What 2 Watch</h1>
{% match user %}
{% when Some with (usr) %}
<p>
Hello, {{ usr.username }}! It's nice to see you. <a href="watches">Let's get watchin'!</a>
</p>
</br>
<p>
<form action="/logout" enctype="application/x-www-form-urlencoded" method="post">
<input type="submit" value="sign out?">
</form>
</p>
{% when None %}
<p>
Heya, why don't you <a href="/login">log in</a> or <a href="/signup">sign up</a>?
</p>
{% endmatch %}
{% endblock %}

10
templates/macros.html Normal file
View file

@ -0,0 +1,10 @@
{% macro get_or_default(val, def) %}
{% match val %}
{% when Some with (v) %}
{{v}}
{% else %}
{{def}}
{% endmatch %}
{% endmacro %}

25
templates/signup.html Normal file
View file

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Welcome, friend, to git.kittenclause.com{% endblock %}
{% block header %} {% endblock %}
{% block content %}
<p>
<form action="/signup" enctype="application/x-www-form-urlencoded" method="post">
<label for="username">Username</label>
<input type="text" name="username" id="username" minlength="1" maxlength="20" required></br>
<label for="displayname">Displayname (optional)</label>
<input type="text" name="displayname" id="displayname"></br>
<label for="email">Email (optional)</label>
<input type="text" name="email"></br>
<label for="password">Password</label>
<input type="password" name="password" id="password" required></br>
<label for="confirm_password">Confirm Password</label>
<input type="password" name="pw_verify" id="pw_verify" required></br>
<input type="submit" value="Signup">
</form>
</p>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Dang, Bish{% endblock %}
{% block content %}
{% block header %}{% endblock %}
<h1>Oh dang!</h1>
<div id="signup_success">
<p>
Sorry, something went wrong: {{self.0}}
</p>
</div>
{% endblock %}

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}Thanks for Signing Up for What 2 Watch, Bish{% endblock %}
{% block content %}
{% block header %}{% endblock %}
<h1>You did it!</h1>
<div id="signup_success"><p>
{{ self.0 }}
</p>
</div>
<p>Now, head on over to <a href="/login">the login page</a> and get watchin'!</p>
{% endblock %}

View file

@ -1,8 +0,0 @@
defmodule QueenTest do
use ExUnit.Case
doctest Queen
test "greets the world" do
assert Queen.hello() == :world
end
end

View file

@ -1 +0,0 @@
ExUnit.start()