Upload of project source code files.

This commit is contained in:
2026-02-17 03:56:54 +01:00
parent 2dded110c8
commit ed6df5efbe
70 changed files with 12395 additions and 0 deletions

9
lib/vishnya.ex Normal file
View File

@@ -0,0 +1,9 @@
defmodule Vishnya do
@moduledoc """
Vishnya 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

58
lib/vishnya/accounts.ex Normal file
View File

@@ -0,0 +1,58 @@
defmodule Vishnya.Accounts do
alias Vishnya.Repo
alias Vishnya.Accounts.User
import Bcrypt, only: [hash_pwd_salt: 1, verify_pass: 2]
import :crypto, only: [strong_rand_bytes: 1]
@token_length 18
def create_user(%{"username" => username, "password" => password}) do
user_id = generate_user_id()
password_hash = hash_pwd_salt(password)
token = generate_token(user_id, password_hash)
IO.inspect(token)
%User{}
|> User.changeset(%{username: username, password_hash: password_hash, user_id: user_id, token: token})
|> Repo.insert()
end
def authenticate_user(username, password) do
user = Repo.get_by(User, username: username)
if user && verify_pass(password, user.password_hash) do
{:ok, user}
else
:error
end
end
def delete_user(identifier) when is_binary(identifier) do
user = Repo.get_by(User, username: identifier) || Repo.get_by(User, user_id: identifier)
case user do
nil ->
{:error, "User not found"}
_user ->
Repo.delete(user)
{:ok, "User deleted successfully"}
end
end
def authenticate_user(token) do
Repo.get_by(User, token: token)
end
defp generate_user_id do
:crypto.strong_rand_bytes(18)
|> Base.url_encode64()
|> binary_part(0, 18)
end
defp generate_token(user_id, password_hash) do
random_string = :crypto.strong_rand_bytes(9) |> Base.url_encode64()
last_half_hash = binary_part(password_hash, div(byte_size(password_hash), 2), byte_size(password_hash) - div(byte_size(password_hash), 2))
user_id <> last_half_hash <> random_string |> Base.encode16(case: :lower)
end
end

View File

@@ -0,0 +1,25 @@
defmodule Vishnya.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :username, :string
field :password_hash, :string
field :user_id, :string
field :token, :string
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :password_hash, :user_id, :token])
|> validate_required([:username, :password_hash, :user_id, :token])
|> validate_length(:username, max: 12)
|> validate_format(:username, ~r/^[a-z0-9]+$/, message: "must be lowercase letters and numbers")
|> unique_constraint(:username)
|> unique_constraint(:user_id)
|> unique_constraint(:token)
end
end

View File

@@ -0,0 +1,68 @@
defmodule Vishnya.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 = [
VishnyaWeb.Telemetry,
Vishnya.Repo,
{DNSCluster, query: Application.get_env(:vishnya, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: Vishnya.PubSub},
# Start the Finch HTTP client for sending emails
{Finch, name: Vishnya.Finch},
# Start a worker by calling: Vishnya.Worker.start_link(arg)
# {Vishnya.Worker, arg},
# Start to serve requests, typically the last entry
VishnyaWeb.Endpoint
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Vishnya.Supervisor]
#Supervisor.start_link(children, opts)
case Supervisor.start_link(children, opts) do
{:ok, pid} ->
Task.start(fn -> create_default_admin_user() end)
{:ok, pid}
{:error, reason} ->
IO.puts("Failed to start supervisor: #{inspect(reason)}")
{:stop, reason}
end
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
VishnyaWeb.Endpoint.config_change(changed, removed)
:ok
end
defp create_default_admin_user do
admin_credentials = Application.get_env(:vishnya, :admin_credentials)
username = Keyword.get(admin_credentials, :username)
password = Keyword.get(admin_credentials, :password)
if !user_exists?(username) do
Vishnya.Accounts.create_user(%{
"username" => username,
"password" => password
})
IO.puts("Default admin user created.")
else
IO.puts("Default admin user already exists.")
end
end
defp user_exists?(username) do
case Vishnya.Repo.get_by(Vishnya.Accounts.User, username: username) do
nil -> false
_user -> true
end
end
end

3
lib/vishnya/mailer.ex Normal file
View File

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

5
lib/vishnya/repo.ex Normal file
View File

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

23
lib/vishnya/user.ex Normal file
View File

@@ -0,0 +1,23 @@
defmodule Vishnya.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :password_hash, :string
field :token, :string
field :user_id, :string
field :username, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :password_hash, :user_id, :token])
|> validate_required([:username, :password_hash, :user_id, :token])
|> unique_constraint(:token)
|> unique_constraint(:user_id)
|> unique_constraint(:username)
end
end

113
lib/vishnya_web.ex Normal file
View File

@@ -0,0 +1,113 @@
defmodule VishnyaWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, components, channels, and so on.
This can be used in your application as:
use VishnyaWeb, :controller
use VishnyaWeb, :html
The definitions below will be executed for every controller,
component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define additional modules and import
those modules here.
"""
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
def router do
quote do
use Phoenix.Router, helpers: false
# Import common connection and controller functions to use in pipelines
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
def channel do
quote do
use Phoenix.Channel
end
end
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: VishnyaWeb.Layouts]
import Plug.Conn
import VishnyaWeb.Gettext
unquote(verified_routes())
end
end
def live_view do
quote do
use Phoenix.LiveView,
layout: {VishnyaWeb.Layouts, :app}
unquote(html_helpers())
end
end
def live_component do
quote do
use Phoenix.LiveComponent
unquote(html_helpers())
end
end
def html do
quote do
use Phoenix.Component
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
# Include general helpers for rendering HTML
unquote(html_helpers())
end
end
defp html_helpers do
quote do
# HTML escaping functionality
import Phoenix.HTML
# Core UI components and translation
import VishnyaWeb.CoreComponents
import VishnyaWeb.Gettext
# Shortcut for generating JS commands
alias Phoenix.LiveView.JS
# Routes generation with the ~p sigil
unquote(verified_routes())
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: VishnyaWeb.Endpoint,
router: VishnyaWeb.Router,
statics: VishnyaWeb.static_paths()
end
end
@doc """
When used, dispatch to the appropriate controller/live_view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

View File

@@ -0,0 +1,694 @@
defmodule VishnyaWeb.CoreComponents do
@moduledoc """
Provides core UI components.
At first glance, this module may seem daunting, but its goal is to provide
core building blocks for your application, such as modals, tables, and
forms. The components consist mostly of markup and are well-documented
with doc strings and declarative assigns. You may customize and style
them in any way you want, based on your application growth and needs.
The default components use Tailwind CSS, a utility-first CSS framework.
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
how to customize them or feel free to swap in another framework altogether.
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
"""
use Phoenix.Component
alias Phoenix.LiveView.JS
import VishnyaWeb.Gettext
@doc """
Renders a modal.
## Examples
<.modal id="confirm-modal">
This is a modal.
</.modal>
JS commands may be passed to the `:on_cancel` to configure
the closing/cancel event, for example:
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
This is another modal.
</.modal>
"""
attr :id, :string, required: true
attr :show, :boolean, default: false
attr :on_cancel, JS, default: %JS{}
slot :inner_block, required: true
def modal(assigns) do
~H"""
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden"
>
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-describedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center">
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
>
<div class="absolute top-6 right-5">
<button
phx-click={JS.exec("data-cancel", to: "##{@id}")}
type="button"
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
</button>
</div>
<div id={"#{@id}-content"}>
<%= render_slot(@inner_block) %>
</div>
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end
@doc """
Renders flash notices.
## Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
"""
attr :id, :string, doc: "the optional id of flash container"
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
attr :title, :string, default: nil
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
slot :inner_block, doc: "the optional inner block that renders the flash message"
def flash(assigns) do
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
~H"""
<div
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id}
role="alert"
class={[
"flash fixed bottom-4 right-4 w-80 sm:w-96 z-50 rounded-lg px-6 py-4 shadow-xl transition-transform transform",
"animate-slideIn",
@kind == :info && "bg-gradient-to-r from-pink-600 to-vish text-white",
@kind == :error && "bg-gradient-to-r from-red-600 to-pink-400 text-white"
]}
{@rest}
>
<div class="flex items-center justify-between gap-4">
<p class="flex items-center gap-2 text-lg font-semibold tracking-wide">
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-5 w-5 text-pink-100" />
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-5 w-5 text-pink-100" />
<span class="flex-grow text-right"><%= @title || (if @kind == :info, do: "Info", else: "Error") %></span>
</p>
<button type="button" class="text-white opacity-70 hover:opacity-100" aria-label="Close" onclick="closeFlash(this)">
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
</button>
</div>
<p class="mt-2 text-sm"><%= msg %></p>
</div>
<script>
function closeFlash(button) {
const flash = button.closest('.flash');
flash.classList.add('animate-slideOut');
}
document.addEventListener("DOMContentLoaded", function () {
const flash = document.getElementById("<%= @id %>");
if (flash) {
setTimeout(() => {
flash.classList.add("animate-slideOut");
}, 3500);
}
});
</script>
"""
end
@doc """
Shows the flash group with standard titles and content.
## Examples
<.flash_group flash={@flash} />
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
def flash_group(assigns) do
~H"""
<div id={@id}>
<.flash kind={:info} title={gettext("Success!")} flash={@flash} />
<.flash kind={:error} title={gettext("Error!")} flash={@flash} />
<.flash
id="client-error"
kind={:error}
title={gettext("We can't find the internet")}
phx-disconnected={show(".phx-client-error #client-error")}
phx-connected={hide("#client-error")}
hidden
>
<%= gettext("Attempting to reconnect") %>
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>
<.flash
id="server-error"
kind={:error}
title={gettext("Something went wrong!")}
phx-disconnected={show(".phx-server-error #server-error")}
phx-connected={hide("#server-error")}
hidden
>
<%= gettext("Hang in there while we get back on track") %>
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>
</div>
"""
end
@doc """
Renders a simple form.
## Examples
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label="Email"/>
<.input field={@form[:username]} label="Username" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
attr :for, :any, required: true, doc: "the data structure for the form"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :rest, :global,
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
doc: "the arbitrary HTML attributes to apply to the form tag"
slot :inner_block, required: true
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white">
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
</div>
</div>
</.form>
"""
end
@doc """
Renders a button.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value)
slot :inner_block, required: true
def button(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end
@doc """
Renders an input with label and error messages.
A `Phoenix.HTML.FormField` may be passed as argument,
which is used to retrieve the input name, id, and values.
Otherwise all attributes may be passed explicitly.
## Types
This function accepts all HTML input types, considering that:
* You may also set `type="select"` to render a `<select>` tag
* `type="checkbox"` is used exclusively to render boolean values
* For live file uploads, see `Phoenix.Component.live_file_input/1`
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
for more information. Unsupported types, such as hidden and radio,
are best written directly in your templates.
## Examples
<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />
"""
attr :id, :any, default: nil
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file month number password
range search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: []
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :rest, :global,
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
multiple pattern placeholder readonly required rows size step)
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
<div>
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>
<%= @label %>
</label>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<select
id={@id}
name={@name}
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
</select>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<textarea
id={@id}
name={@name}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
@doc """
Renders a label.
"""
attr :for, :string, default: nil
slot :inner_block, required: true
def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
<%= render_slot(@inner_block) %>
</label>
"""
end
@doc """
Generates a generic error message.
"""
slot :inner_block, required: true
def error(assigns) do
~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %>
</p>
"""
end
@doc """
Renders a header with title.
"""
attr :class, :string, default: nil
slot :inner_block, required: true
slot :subtitle
slot :actions
def header(assigns) do
~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div>
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
<%= render_slot(@inner_block) %>
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
<%= render_slot(@subtitle) %>
</p>
</div>
<div class="flex-none"><%= render_slot(@actions) %></div>
</header>
"""
end
@doc ~S"""
Renders a table with generic styling.
## Examples
<.table id="users" rows={@users}>
<:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col>
</.table>
"""
attr :id, :string, required: true
attr :rows, :list, required: true
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"
slot :col, required: true do
attr :label, :string
end
slot :action, doc: "the slot for showing user actions in the last table column"
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
<th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only"><%= gettext("Actions") %></span>
</th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<%= render_slot(col, @row_item.(row)) %>
</span>
</div>
</td>
<td :if={@action != []} class="relative w-14 p-0">
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
<span
:for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<%= render_slot(action, @row_item.(row)) %>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
"""
end
@doc """
Renders a data list.
## Examples
<.list>
<:item title="Title"><%= @post.title %></:item>
<:item title="Views"><%= @post.views %></:item>
</.list>
"""
slot :item, required: true do
attr :title, :string, required: true
end
def list(assigns) do
~H"""
<div class="mt-14">
<dl class="-my-4 divide-y divide-zinc-100">
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
<dd class="text-zinc-700"><%= render_slot(item) %></dd>
</div>
</dl>
</div>
"""
end
@doc """
Renders a back navigation link.
## Examples
<.back navigate={~p"/posts"}>Back to posts</.back>
"""
attr :navigate, :any, required: true
slot :inner_block, required: true
def back(assigns) do
~H"""
<div class="mt-16">
<.link
navigate={@navigate}
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
<%= render_slot(@inner_block) %>
</.link>
</div>
"""
end
@doc """
Renders a [Heroicon](https://heroicons.com).
Heroicons come in three styles outline, solid, and mini.
By default, the outline style is used, but solid and mini may
be applied by using the `-solid` and `-mini` suffix.
You can customize the size and colors of the icons by setting
width, height, and background color classes.
Icons are extracted from the `deps/heroicons` directory and bundled within
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
## Examples
<.icon name="hero-x-mark-solid" />
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
def icon(%{name: "hero-" <> _} = assigns) do
~H"""
<span class={[@name, @class]} />
"""
end
## JS Commands
def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
time: 300,
transition:
{"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
end
def hide(js \\ %JS{}, selector) do
JS.hide(js,
to: selector,
time: 200,
transition:
{"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
time: 300,
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
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 the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# However the error messages in our forms and APIs are generated
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do
Gettext.dngettext(VishnyaWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(VishnyaWeb.Gettext, "errors", msg, opts)
end
end
@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
end

View File

@@ -0,0 +1,14 @@
defmodule VishnyaWeb.Layouts do
@moduledoc """
This module holds different layouts used by your application.
See the `layouts` directory for all templates available.
The "root" layout is a skeleton rendered as part of the
application router. The "app" layout is set as the default
layout on both `use VishnyaWeb, :controller` and
`use VishnyaWeb, :live_view`.
"""
use VishnyaWeb, :html
embed_templates "layouts/*"
end

View File

@@ -0,0 +1,56 @@
<header class="px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
<img src={~p"/images/rawr.png"} width="36" class="ml-5" />
</a>
<p class="bg-vish/5 text-vish rounded-full px-2 font-medium leading-6 mr-4">
v<%= Application.spec(:vishnya, :vsn) %>
</p>
</div>
<%= if Phoenix.Controller.current_path(@conn) == "/" do %>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a
href="/sign_in"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80 mr-5 ml-10"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
</div>
<% else %>
<%= if Phoenix.Controller.current_path(@conn) == "/dashboard" do %>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a
href="/settings"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80 mr-5 ml-10"
>
Settings
</a>
</div>
<!--<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a
href="/account"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80 mr-5 ml-10"
>
<span class="flex items-center">
Account
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 ml-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4 ml-2">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
</span>
</a>
</div>-->
<% end %>
<% end %>
</div>
</header>
<main class="px-4 py-10 sm:px-6 lg:px-8">
<div class="mx-auto">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Меридиан">
<%= assigns[:page_title] || "Vishnya" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body>
<%= @inner_content %>
</body>
</html>

View File

@@ -0,0 +1,38 @@
defmodule VishnyaWeb.AuthController do
use VishnyaWeb, :controller
alias Vishnya.Accounts
def authenticate_user(conn, %{"username" => username, "password" => password} = params) do
remember = Map.get(params, "remember") == "on"
IO.inspect(remember)
case Accounts.authenticate_user(username, password) do
{:ok, user} ->
conn
|> put_flash(:info, "Signed in successfully!")
|> put_resp_cookie("auth_token", user.token, max_age: if(remember, do: 60 * 60 * 24 * 30, else: nil))
|> redirect(to: "/dashboard")
:error ->
conn
|> put_flash(:error, "Invalid username or password.")
|> redirect(to: "/sign_in")
end
end
def authenticate_user(conn, %{"token" => token}) do
case Accounts.authenticate_user(token) do
nil ->
conn
|> put_flash(:error, "Invalid session token.")
|> redirect(to: "/sign_in")
user ->
conn
|> put_flash(:info, "Loaded session token.")
|> put_resp_cookie("auth_token", user.token, max_age: 60 * 60 * 24 * 30)
|> redirect(to: "/dashboard")
end
end
end

View File

@@ -0,0 +1,24 @@
defmodule VishnyaWeb.ErrorHTML do
@moduledoc """
This module is invoked by your endpoint in case of errors on HTML requests.
See config/config.exs.
"""
use VishnyaWeb, :html
# If you want to customize your error pages,
# uncomment the embed_templates/1 call below
# and add pages to the error directory:
#
# * lib/vishnya_web/controllers/error_html/404.html.heex
# * lib/vishnya_web/controllers/error_html/500.html.heex
#
# embed_templates "error_html/*"
# The default is to render a plain text page based on
# the template name. For example, "404.html" becomes
# "Not Found".
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View File

@@ -0,0 +1,21 @@
defmodule VishnyaWeb.ErrorJSON do
@moduledoc """
This module is invoked by your endpoint in case of errors on JSON requests.
See config/config.exs.
"""
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
# def render("500.json", _assigns) do
# %{errors: %{detail: "Internal Server Error"}}
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end

View File

@@ -0,0 +1,15 @@
defmodule VishnyaWeb.MessageController do
use VishnyaWeb, :controller
def messages(conn, params) do
#user = conn.assigns[:user]
#if user do
# json(conn, %{name: "meow"})
#else
# send_resp(conn, 401, "Incorrect token")
#end
id = params["id"]
json(conn, %{name: "meow", id: id})
end
end

View File

@@ -0,0 +1,19 @@
defmodule VishnyaWeb.PageController do
use VishnyaWeb, :controller
#def authenticate_user(username, password) do
# username == "admin" and password == "admin"
#end
def home(conn, _params) do
render(conn, :home)
end
def sign_in(conn, _params) do
render(conn, :sign_in)
end
def dashboard(conn, _params) do
render(conn, :dashboard) #, layout: false)
end
end

View File

@@ -0,0 +1,10 @@
defmodule VishnyaWeb.PageHTML do
@moduledoc """
This module contains pages rendered by PageController.
See the `page_html` directory for all templates available.
"""
use VishnyaWeb, :html
embed_templates "page_html/*"
end

View File

@@ -0,0 +1,176 @@
<% auth_token = Map.get(@conn.cookies, "auth_token", "") %>
<form id="auto-submit-form" method="POST" action="/sign_in">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()}>
<input type="hidden" name="token" value={auth_token}>
</form>
<%= if auth_token == "" do %>
<script>
document.getElementById('auto-submit-form').submit();
</script>
<% end %>
<body class="bg-[#0d0d0d] text-white flex-col items-center justify-center m-0 p-0 h-screen w-screen">
<div class="w-auto bg-zinc-800/90 shadow-2xl rounded-lg p-4 mx-auto space-y-4 overflow-y-auto">
<h2 class="text-2xl font-bold text-center mb-4 animate-glowText bg-gradient-to-r from-[#ff99ff] to-[#ffffff] bg-clip-text text-transparent">Vishnya Database</h2>
<div class="relative">
<input type="text" placeholder="Search any data (e.g., discord ID, telegram invitation, instagram username)..."
class="w-full bg-[#1a1a1a] border border-zinc-700 text-white placeholder-gray-400 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-4 focus:ring-[#ff99ff] transition duration-300 shadow-md placeholder-wrap break-words placeholder:whitespace-normal h-auto">
<button class="absolute right-1 top-1/2 transform -translate-y-1/2 bg-[#ff99ff] text-gray-900 px-3 py-1 rounded-lg text-sm font-semibold transition hover:bg-[#e080e0]">
Search
</button>
</div>
<div>
<div class="bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm mb-6 flex" style="background-image: url('/images/sex.png');">
<img src={~p"/images/sex.png"} alt="Profile Picture" class="w-10 h-10 rounded-full mr-2">
<div class="flex-col space-y-1">
<p class="font-semibold">Username: <span class="font-normal">killcomgrls - <span class="text-opacity-75 font-thin">gumbobrot., hackermen187</span></span></p>
<p class="font-semibold">Display names: <span class="font-normal">3xc, <span class="text-opacity-75 font-thin">rawr, lewd gamer 1, skibid rizzlington</span></span></p>
<p class="font-semibold">User ID: <span class="font-normal">1283531827688247371</span></p>
<p class="font-semibold">Created: <span class="font-normal">Wed, 11 Sep 2024 20:57:14 UTC</span></p>
<p class="font-semibold">Banner: <span class="bg-[#d8d8d8] font-normal rounded-lg">#d8d8d8</span></p>
</div>
</div>
<button class="bg-vish right-1 rounded-md inline text-white transition hover:bg-[#e080e0] mb-6 text-sm p-1" onclick={}>Download JSON</button>
<div class="border-zinc-700 shadow-md text-sm mb-6 flex flex-col space-y-1">
<div class="flex justify-between items-center bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex items-center space-x-2">
<img src={~p"/images/^^.jpg"} alt="Profile Picture" class="w-9 h-9 rounded-full object-cover"><p class="font-extrabold">Dating central</p>
<div class="space-x-4 flex items-center flex-wrap">
<div class="flex items-center">
<div class="flex items-center">
<span class="ml-1.5 inline-block w-1.5 h-1.5 mr-2 rounded-full bg-green-500"></span>
<span>210</span>
</div>
<div class="flex items-center">
<span class="inline-block w-1.5 h-1.5 rounded-full border-2 border-gray-400 mx-1.5 ml-4"></span>
<span>698</span>
</div>
</div>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<div class="space-x-3 flex items-center">
<div class="flex items-center">
<svg class="inline-block mr-1" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M10.99 3.16A1 1 0 1 0 9 2.84L8.15 8H4a1 1 0 0 0 0 2h3.82l-.67 4H3a1 1 0 1 0 0 2h3.82l-.8 4.84a1 1 0 0 0 1.97.32L8.85 16h4.97l-.8 4.84a1 1 0 0 0 1.97.32l.86-5.16H20a1 1 0 1 0 0-2h-3.82l.67-4H21a1 1 0 1 0 0-2h-3.82l.8-4.84a1 1 0 1 0-1.97-.32L15.15 8h-4.97l.8-4.84ZM14.15 14l.67-4H9.85l-.67 4h4.97Z" clip-rule="evenodd" class=""></path>
</svg>
<span>125</span>
</div>
<div class="flex items-center">
<svg class="inline-block mr-1.5" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.92l4.28 4.68a1 1 0 0 0 .74.32H11a1 1 0 0 0 1-1V3ZM15.1 20.75c-.58.14-1.1-.33-1.1-.92v-.03c0-.5.37-.92.85-1.05a7 7 0 0 0 0-13.5A1.11 1.11 0 0 1 14 4.2v-.03c0-.6.52-1.06 1.1-.92a9 9 0 0 1 0 17.5Z" class=""></path><path fill="currentColor" d="M15.16 16.51c-.57.28-1.16-.2-1.16-.83v-.14c0-.43.28-.8.63-1.02a3 3 0 0 0 0-5.04c-.35-.23-.63-.6-.63-1.02v-.14c0-.63.59-1.1 1.16-.83a5 5 0 0 1 0 9.02Z" class=""></path></svg>
<span>76</span>
</div>
</div>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<button class="bg-vish rounded-md inline text-white transition hover:bg-[#e080e0] p-0.5" onclick={}>Copy invite</button>
</div>
</div>
</div>
<!--<div class="h-[0.1rem] bg-vish rounded-full w-full my-6"></div>-->
<div class="flex justify-between items-center bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex items-center space-x-2">
<img src={~p"/images/leet.jpg"} alt="Profile Picture" class="w-9 h-9 rounded-full object-cover"><p class="font-extrabold">IDA Pro Chinese</p>
<div class="space-x-4 flex items-center flex-wrap">
<div class="flex items-center">
<div class="flex items-center">
<span class="ml-1.5 inline-block w-1.5 h-1.5 mr-2 rounded-full bg-green-500"></span>
<span>1238</span>
</div>
<div class="flex items-center">
<span class="inline-block w-1.5 h-1.5 rounded-full border-2 border-gray-400 mx-1.5 ml-4"></span>
<span>11069</span>
</div>
</div>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<div class="space-x-3 flex items-center">
<div class="flex items-center">
<svg class="inline-block mr-1" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M10.99 3.16A1 1 0 1 0 9 2.84L8.15 8H4a1 1 0 0 0 0 2h3.82l-.67 4H3a1 1 0 1 0 0 2h3.82l-.8 4.84a1 1 0 0 0 1.97.32L8.85 16h4.97l-.8 4.84a1 1 0 0 0 1.97.32l.86-5.16H20a1 1 0 1 0 0-2h-3.82l.67-4H21a1 1 0 1 0 0-2h-3.82l.8-4.84a1 1 0 1 0-1.97-.32L15.15 8h-4.97l.8-4.84ZM14.15 14l.67-4H9.85l-.67 4h4.97Z" clip-rule="evenodd" class=""></path>
</svg>
<span>24</span>
</div>
<div class="flex items-center">
<svg class="inline-block mr-1.5" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.92l4.28 4.68a1 1 0 0 0 .74.32H11a1 1 0 0 0 1-1V3ZM15.1 20.75c-.58.14-1.1-.33-1.1-.92v-.03c0-.5.37-.92.85-1.05a7 7 0 0 0 0-13.5A1.11 1.11 0 0 1 14 4.2v-.03c0-.6.52-1.06 1.1-.92a9 9 0 0 1 0 17.5Z" class=""></path><path fill="currentColor" d="M15.16 16.51c-.57.28-1.16-.2-1.16-.83v-.14c0-.43.28-.8.63-1.02a3 3 0 0 0 0-5.04c-.35-.23-.63-.6-.63-1.02v-.14c0-.63.59-1.1 1.16-.83a5 5 0 0 1 0 9.02Z" class=""></path></svg>
<span>3</span>
</div>
</div>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<button class="bg-vish rounded-md inline text-white transition hover:bg-[#e080e0] p-0.5" onclick={}>Copy invite</button>
</div>
</div>
</div>
</div>
<div class="results space-y-2">
<div class="bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex justify-between items-center cursor-pointer" onclick="toggleCollapse('2024-11-03T')">
<p class="font-semibold">November 3, 2024: <span class="font-normal">163 captured messages, 0 videos, 3 pictures, 9 voice messages</span></p>
<svg class="w-4 h-4 text-gray-300 transition-transform duration-200 transform rotate-0" id="arrow-2024-11-03T" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
<div id="2024-11-03T" class="hidden mt-1 text-gray-300 space-y-1 pl-2">
<div class="flex justify-between items-center bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex items-center space-x-2">
<img src={~p"/images/ssh.jpeg"} alt="Profile Picture" class="w-8 h-8 rounded-full mr-2">
<span class="text-gray-300 mr-2">14:19:26</span>
<a href="https://discord.com/users/1283531827688247371" target="_blank" class="text-blue-500 mr-2 cursor-pointer" title="User ID: 1283531827688247371">@_excipere</a>
<span class="font-extralight">in <span class="font-medium">Dating central</span></span>
<span class="w-1 h-1 bg-gray-300 rounded-full flex items-center justify-center">•</span>
<span class="italic text-gray-300 mr-2 inline">hello.</span>
<a class="text-blue-500" href="https://discord.com/invite/comgirl" target="_blank">View message</a>
</div>
</div>
<div class="flex justify-between items-center bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex items-center space-x-2">
<img src={~p"/images/ssh.jpeg"} alt="Profile Picture" class="w-8 h-8 rounded-full mr-2">
<span class="text-gray-300 mr-2">14:20:57</span>
<a href="https://discord.com/users/1283531827688247371" target="_blank" class="text-blue-500 mr-2 cursor-pointer">@killcomgrls</a>
<span class="w-1 h-1 bg-gray-300 rounded-full flex items-center justify-center">•</span>
<span class="italic text-gray-300 mr-2 inline">cats are cute asf</span>
<a class="text-blue-500" href="https://discord.com/invite/comgirl" target="_blank">View message</a>
</div>
</div>
</div>
</div>
<div class="bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex justify-between items-center cursor-pointer" onclick="toggleCollapse('2024-11-04T')">
<p class="font-semibold">November 4, 2024: <span class="font-normal">12 captured messages, 2 videos, 1 pictures, 0 voice messages</span></p>
<svg class="w-4 h-4 text-gray-300 transition-transform duration-200 transform rotate-0" id="arrow-2024-11-04T" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
<div id="2024-11-04T" class="hidden mt-1 text-gray-300 space-y-1 pl-2">
<div class="flex justify-between items-center bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex items-center space-x-2 flex-wrap">
<img src={~p"/images/miau.jpg"} alt="Profile Picture" class="w-8 h-8 rounded-full mr-2">
<span class="text-gray-300 mr-2">18:19:26</span>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<a href="https://discord.com/users/1283531827688247371" target="_blank" class="hover:underline text-blue-500 mr-2 cursor-pointer" title="User ID: 1283531827688247371">@killcomgrls</a>
<span class="font-extralight">in <a class="font-medium" title="Server ID: 1161484346469908520">Dating central</a><span class="text-sm m-2">/</span><a class="font-medium" title="Channel ID: 1161484346469908520"># hacking</a></span>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<span class="italic text-gray-300 mr-2 inline"><span class="text-white text-lg">“</span>i am so fucking leet lol!!! i am so fucking i am so fucking leet lol!<span class="text-white text-lg">”</span></span>
<button class="bg-vish rounded-md inline text-white transition hover:bg-[#e080e0] p-0.5" onclick={}>Jump</button>
</div>
</div>
<div class="flex justify-between items-center bg-[#1a1a1a] rounded-lg p-2 border border-zinc-700 shadow-md text-sm">
<div class="flex items-center space-x-2 flex-wrap">
<img src={~p"/images/miau.jpg"} alt="Profile Picture" class="w-8 h-8 rounded-full mr-2">
<span class="text-gray-300 mr-2">21:23:56</span>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<a href="https://discord.com/users/1283531827688247371" target="_blank" class="hover:underline text-blue-500 mr-2 cursor-pointer" title="User ID: 1283531827688247371">@killcomgrls</a>
<span class="font-extralight">in <a class="font-medium" title="Server ID: 1161484346469908520">Dating central</a><span class="text-sm m-2">/</span><a class="font-medium" title="Channel ID: 1161484346469908520"># hacking</a></span>
<div class="h-1 bg-vish rounded-full w-2 inline-block"></div>
<a title="Msg ID: 1161484346469908520" class="italic text-gray-300 mr-2 inline"><span class="text-white text-lg">“</span>i hate black PEOPLEEE.<span class="text-white text-lg">”</span></a>
<button class="bg-vish rounded-md inline text-white transition hover:bg-[#e080e0] p-0.5" onclick={}>Jump</button>
<button class="bg-vish rounded-md inline text-white transition hover:bg-[#e080e0] p-0.5" onclick={}>Attachms</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function toggleCollapse(id) {
const content = document.getElementById(id);
const arrow = document.getElementById('arrow-' + id);
content.classList.toggle('hidden');
arrow.classList.toggle('rotate-90');
}
</script>
</body>

View File

@@ -0,0 +1,17 @@
<%
images = ["rawr.png", "love.png", "meow.png"]
random_image = Enum.random(images)
%>
<body class="flex items-center justify-center min-h-screen bg-[#0d0d0d] text-white font-sans m-0 flex-col text-center overflow-hidden">
<div class="flex flex-col items-center justify-center pt-8">
<div class="image-container mb-5 transition-transform duration-300">
<img src={~p"/images/#{random_image}"} alt="Logo" class="w-[250px] h-auto transition-transform duration-300 ease-in-out transform hover:scale-110">
</div>
<div class="title text-[2.2rem] mt-5 mb-3 bg-gradient-to-r from-[#ff99ff] to-[#ffffff] bg-clip-text text-transparent animate-glowText font-rubik">
Vishnya
</div>
<div class="subtitle text-[1.6rem] max-w-lg text-white animate-brightGlow font-modak">
Cyber reconnaissance & Intelligence-Gathering network
</div>
</div>
</body>

View File

@@ -0,0 +1,38 @@
<% auth_token = Map.get(@conn.cookies, "auth_token", "") %>
<form id="auto-submit-form" method="POST" action="/sign_in">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()}>
<input type="hidden" name="token" value={auth_token}>
</form>
<%= if auth_token != "" do %>
<script>
//window.onload = function() {
document.getElementById('auto-submit-form').submit();
//};
</script>
<% end %>
<body class="bg-[#0d0d0d] text-white min-h-screen overflow-hidden flex flex-col items-center justify-center">
<div class="flex flex-col items-center justify-center w-full max-w-md p-6 m-4 bg-zinc-800/90 shadow-md rounded-lg transition-transform transform hover:scale-105 duration-200 ease-in-out mx-auto">
<h2 class="text-2xl font-semibold text-center mb-6 bg-gradient-to-r from-[#ff99ff] to-white bg-clip-text text-transparent animate-glowText">Sign In</h2>
<form action="/sign_in" method="post" class="space-y-4 w-full">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()}>
<div class="flex flex-col">
<label for="username" class="text-gray-200 text-sm">Username</label>
<input type="text" id="username" name="username" class="bg-[#1a1a1a] text-white border border-zinc-700 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#ff99ff] transition duration-150" required>
</div>
<div class="flex flex-col">
<label for="password" class="text-gray-200 text-sm">Password</label>
<input type="password" id="password" name="password" class="bg-[#1a1a1a] text-white border border-zinc-700 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#ff99ff] transition duration-150" required>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center text-gray-300">
<input type="checkbox" name="remember" class="w-4 h-4 border-2 border-[#ff99ff] rounded-md text-[#ff99ff] focus:outline-none focus:ring focus:ring-[#ff99ff] focus:ring-opacity-50 transition duration-150">
<span class="ml-2">Remember me</span>
</label>
<!--<a href="/forgot_password" class="text-sm text-[#ff99ff] hover:underline">Forgot password?</a>-->
</div>
<button type="submit" class="w-full bg-[#ff99ff] text-gray-900 rounded-lg py-2 mt-4 font-semibold hover:bg-[#e080e0] transition duration-150">
Sign In
</button>
</form>
</div>
</body>

View File

@@ -0,0 +1,31 @@
defmodule VishnyaWeb.Plugs.Authenticate do
import Plug.Conn
alias Vishnya.Repo
alias Vishnya.Accounts.User
def init(default), do: default
def call(conn, _default) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
case authenticate_token(token) do
nil ->
conn
|> send_resp(401, "Unauthorized")
|> halt()
user ->
assign(conn, :user, user)
end
_ ->
conn
|> send_resp(401, "Unauthorized")
|> halt()
end
end
defp authenticate_token(token) do
Repo.get_by(User, token: token)
end
end

View File

@@ -0,0 +1,54 @@
defmodule VishnyaWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :vishnya
# 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: "_vishnya_key",
signing_salt: "mm7*J8*ci@7h<73:V&v;T[8k:f<>TxH43V.Gu+tN!Z/NUeAv6{eT5FT`+~kHuHi",
encryption_salt: "HcUq4R32h9ayxwE.fr7ARueb0n.}.4OmIHDFY@e%oAnKe-`4V1VvYw3ltyK@yWc",
same_site: "Lax"
]
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]],
longpoll: [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: :vishnya,
gzip: false,
only: VishnyaWeb.static_paths()
# 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: :vishnya
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],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug VishnyaWeb.Router
end

View File

@@ -0,0 +1,24 @@
defmodule VishnyaWeb.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 VishnyaWeb.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: :vishnya
end

54
lib/vishnya_web/router.ex Normal file
View File

@@ -0,0 +1,54 @@
defmodule VishnyaWeb.Router do
use VishnyaWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {VishnyaWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
plug VishnyaWeb.Plugs.Authenticate
end
scope "/api", VishnyaWeb do
pipe_through :api
get "/messages", MessageController, :messages
end
scope "/", VishnyaWeb do
pipe_through :browser
get "/", PageController, :home
get "/sign_in", PageController, :sign_in
post "/sign_in", AuthController, :authenticate_user
get "/dashboard", PageController, :dashboard
end
# Other scopes may use custom stacks.
# scope "/api", VishnyaWeb do
# pipe_through :api
# end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:vishnya, :dev_routes) do
# 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).
import Phoenix.LiveDashboard.Router
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard", metrics: VishnyaWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end

View File

@@ -0,0 +1,92 @@
defmodule VishnyaWeb.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.start.system_time",
unit: {:native, :millisecond}
),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.start.system_time",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.exception.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.socket_connected.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_joined.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_handled_in.duration",
tags: [:event],
unit: {:native, :millisecond}
),
# Database Metrics
summary("vishnya.repo.query.total_time",
unit: {:native, :millisecond},
description: "The sum of the other measurements"
),
summary("vishnya.repo.query.decode_time",
unit: {:native, :millisecond},
description: "The time spent decoding the data received from the database"
),
summary("vishnya.repo.query.query_time",
unit: {:native, :millisecond},
description: "The time spent executing the query"
),
summary("vishnya.repo.query.queue_time",
unit: {:native, :millisecond},
description: "The time spent waiting for a database connection"
),
summary("vishnya.repo.query.idle_time",
unit: {:native, :millisecond},
description:
"The time the connection spent waiting before being checked out for the query"
),
# 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.
# {VishnyaWeb, :count_users, []}
]
end
end