feat: challenge registry-driven platform + XVI/XVII imports + admin toggle + domain rename

- Rename repo/domain: attack-defense-platform / attackdefense.imrnes.team (all refs replaced)
- challenge_registry.json: single source of truth (28 challs across gemastik18/xvi/xvii)
- teams.py: registry-driven CHALLENGES, set_challenge_enabled, sync_challenge_runtime
  (apply enable/disable to live teams: build/up or stop/remove + receiver restart)
- compose_gen.py: render per-team compose from canonical per-challenge templates
  (image reuse, per-team ports 30xxx, flag mounts, passwords)
- gen_canonical_composes.py: canonical docker-compose.yml for all services
- import_new_challenges.py: import XVI/XVII services + EOL base image fixes
  (debian:buster→bookworm, node:14→20, python:3.7-slim→3.11)
- receiver: xvi package (10 checkers) + xvii package (12 generic checkers),
  Challenge base reads PASSWORD_<team_port> from env; gen_receiver_main.py
  generates per-team main.py from registry
- main.py: /api/challenges returns full registry; PATCH /api/challenges/<name>
  toggles enabled + applies to live teams
- index.html: 🏗️ Challenge Manager tab (toggle per challenge, grouped by set)
- SLA bonus now dynamic (all enabled challenges, not hardcoded 6)
This commit is contained in:
MythEclipse committed 2026-09-25 14:04:33 +08:00
1 parent c35b23a37f
commit c6fd9ec268
1317 files changed
+306586 -128

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
import gleam/result
import simplifile
pub const upload_dir = "/tmp/gleam-drive/"
pub const max_file_size = 524_288
pub fn aes_key() -> String {
result.unwrap(simplifile.read(".aes-key"), "change-me-please")
}
+54
View File
@@ -0,0 +1,54 @@
import gleam/bit_array
import gleam/result
import gleam/string
const block_size = 16
@external(erlang, "crypto_ffi", "encrypt")
pub fn encrypt_ffi(key: BitArray, data: BitArray) -> BitArray
@external(erlang, "crypto_ffi", "decrypt")
pub fn decrypt_ffi(key: BitArray, data: BitArray) -> BitArray
@external(erlang, "crypto_ffi", "hash")
pub fn hash(data: String) -> String
pub fn encrypt(key: String, data: String) -> String {
let key = bit_array.from_string(key)
let data = bit_array.from_string(pad(data))
let ciphertext = encrypt_ffi(key, data)
bit_array.base16_encode(ciphertext)
}
pub fn decrypt(key: String, data: String) -> String {
let key = bit_array.from_string(key)
let data = bit_array.base16_decode(data)
case data {
Ok(data) -> {
let plaintext = decrypt_ffi(key, data)
unpad(result.unwrap(bit_array.to_string(plaintext), ""))
}
Error(_) -> ""
}
}
pub fn pad(data: String) -> String {
let remainder = string.byte_size(data) % block_size
let padding = string.repeat(" ", block_size - remainder)
string.append(data, padding)
}
pub fn unpad(data: String) -> String {
string.trim_right(data)
}
pub fn encode(data: String) -> String {
bit_array.base16_encode(bit_array.from_string(data))
}
pub fn decode(data: String) -> String {
case bit_array.base16_decode(data) {
Ok(data) -> result.unwrap(bit_array.to_string(data), "")
Error(_) -> ""
}
}
+54
View File
@@ -0,0 +1,54 @@
import gleam/dynamic
import gleam/json.{int, object, string}
pub type User {
User(email: String, password: String)
}
pub type Session {
Session(email: String, expired_at: Int)
}
pub type Upload {
Upload(email: String, filename: String, filesize: Int, timestamp: String)
}
pub type Shared {
Shared(filepath: String, recepient: String)
}
pub fn session_to_json(session: Session) -> String {
object([
#("email", string(session.email)),
#("expired_at", int(session.expired_at)),
])
|> json.to_string
}
pub fn session_from_json(data: String) -> Result(Session, json.DecodeError) {
let session_decoder =
dynamic.decode2(
Session,
dynamic.field("email", of: dynamic.string),
dynamic.field("expired_at", of: dynamic.int),
)
json.decode(from: data, using: session_decoder)
}
pub fn shared_to_json(shared: Shared) -> String {
object([
#("filepath", string(shared.filepath)),
#("recepient", string(shared.recepient)),
])
|> json.to_string
}
pub fn shared_from_json(data: String) -> Result(Shared, json.DecodeError) {
let shared_decoder =
dynamic.decode2(
Shared,
dynamic.field("filepath", of: dynamic.string),
dynamic.field("recepient", of: dynamic.string),
)
json.decode(from: data, using: shared_decoder)
}
@@ -0,0 +1,153 @@
import app/crypto
import app/model
import gleam/dynamic
import gleam/int
import gleam/list
import sqlight
pub fn init(db: sqlight.Connection) {
let assert Ok(Nil) =
sqlight.exec(
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT, password TEXT)",
db,
)
let assert Ok(Nil) =
sqlight.exec(
"CREATE TABLE IF NOT EXISTS uploads (id INTEGER PRIMARY KEY, email TEXT, filename TEXT, filesize INTEGER, timestamp TEXT)",
db,
)
let assert Ok(Nil) =
sqlight.exec(
"CREATE INDEX IF NOT EXISTS uploads_owner_index ON uploads (email)",
db,
)
}
pub fn login(db: sqlight.Connection, email: String, password: String) -> Bool {
let sql = "SELECT * FROM users WHERE email = ? AND password = ?"
let user_decoder = dynamic.tuple3(dynamic.int, dynamic.string, dynamic.string)
let assert Ok(users) =
sqlight.query(
sql,
on: db,
with: [
sqlight.text(crypto.encode(email)),
sqlight.text(crypto.hash(password)),
],
expecting: user_decoder,
)
list.length(users) > 0
}
pub fn create_user(
db: sqlight.Connection,
email: String,
password: String,
) -> Result(String, String) {
let sql =
"INSERT INTO users (email, password) VALUES ('"
<> crypto.encode(email)
<> "', '"
<> crypto.hash(password)
<> "')"
case sqlight.exec(sql, db) {
Ok(_) -> Ok("User created")
Error(_) -> Error("Failed to create user")
}
}
pub fn user_exists(db: sqlight.Connection, email: String) -> Bool {
let sql = "SELECT * FROM users WHERE email = ?"
let user_decoder = dynamic.tuple3(dynamic.int, dynamic.string, dynamic.string)
let assert Ok(users) =
sqlight.query(
sql,
on: db,
with: [sqlight.text(crypto.encode(email))],
expecting: user_decoder,
)
list.length(users) > 0
}
pub fn create_upload(
db: sqlight.Connection,
email: String,
filename: String,
filesize: Int,
) {
let sql =
"INSERT INTO uploads (email, filename, filesize, timestamp) VALUES ('"
<> crypto.encode(email)
<> "', '"
<> crypto.encode(filename)
<> "', "
<> int.to_string(filesize)
<> ", datetime('now'))"
let assert Ok(Nil) = sqlight.exec(sql, db)
}
pub fn list_uploads(db: sqlight.Connection, email: String) -> List(model.Upload) {
let sql = "SELECT * FROM uploads WHERE email = ?"
let upload_decoder =
dynamic.tuple5(
dynamic.int,
dynamic.string,
dynamic.string,
dynamic.int,
dynamic.string,
)
let assert Ok(uploads) =
sqlight.query(
sql,
on: db,
with: [sqlight.text(crypto.encode(email))],
expecting: upload_decoder,
)
list.map(uploads, fn(upload) {
model.Upload(
crypto.decode(upload.1),
crypto.decode(upload.2),
upload.3,
upload.4,
)
})
}
pub fn find_file(
db: sqlight.Connection,
email: String,
filename: String,
) -> Result(model.Upload, String) {
let sql = "SELECT * FROM uploads WHERE email = ? AND filename = ?"
let upload_decoder =
dynamic.tuple5(
dynamic.int,
dynamic.string,
dynamic.string,
dynamic.int,
dynamic.string,
)
let assert Ok(uploads) =
sqlight.query(
sql,
on: db,
with: [
sqlight.text(crypto.encode(email)),
sqlight.text(crypto.encode(filename)),
],
expecting: upload_decoder,
)
case list.first(uploads) {
Ok(upload) ->
Ok(model.Upload(
crypto.decode(upload.1),
crypto.decode(upload.2),
upload.3,
upload.4,
))
Error(_) -> Error("File not found")
}
}
+267
View File
@@ -0,0 +1,267 @@
import app/config
import app/model
import app/service
import app/util
import app/view
import app/web
import birl
import filepath
import gleam/http.{Get, Post}
import gleam/io
import gleam/list
import gleam/result
import gleam/string_builder
import wisp.{type Request, type Response}
pub fn handle_request(req: Request, ctx: web.Context) -> Response {
use req <- web.middleware(req)
let session_cookie = case wisp.get_cookie(req, "session", wisp.PlainText) {
Ok(cookie) -> cookie
Error(_) -> ""
}
let session = service.extract_session_token(session_cookie)
case session.expired_at > birl.to_unix(birl.now()) {
True -> handle_protected_routes(req, ctx, session)
False -> handle_routes(req, ctx)
}
}
pub fn handle_routes(req: Request, ctx: web.Context) -> Response {
case wisp.path_segments(req) {
[] -> show_home(req)
["login"] ->
case req.method {
Post -> handle_login(req, ctx)
_ -> show_login()
}
["register"] ->
case req.method {
Post -> handle_register(req, ctx)
_ -> show_register()
}
["error"] -> show_error(req)
_ -> wisp.redirect("/login")
}
}
pub fn handle_protected_routes(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
case wisp.path_segments(req) {
["drive"] ->
case req.method {
Post -> handle_upload(req, ctx, session)
_ -> show_drive(ctx, session)
}
["share"] -> handle_share(req, ctx, session)
["details"] -> handle_details(req, ctx, session)
["download"] -> handle_download(req, session)
["logout"] -> destroy_session(req)
["error"] -> show_error(req)
_ -> wisp.redirect("/drive")
}
}
fn destroy_session(req: Request) -> Response {
let resp = wisp.redirect("/login")
wisp.set_cookie(resp, req, "session", "", wisp.PlainText, 0)
}
fn create_session(req: Request, token: String) -> Response {
let resp = wisp.redirect("/drive")
wisp.set_cookie(resp, req, "session", token, wisp.PlainText, 30 * 60)
}
fn error_response(message: String) -> Response {
wisp.redirect("/error?message=" <> message)
}
fn show_share(download_link: String) -> Response {
view.render_share_html(download_link)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn send_file(shared: model.Shared, session: model.Session) -> Response {
case shared.recepient == session.email {
True -> {
wisp.ok()
|> wisp.set_header("content-type", "application/octet-stream")
|> wisp.file_download(
named: filepath.base_name(shared.filepath),
from: shared.filepath,
)
}
False -> error_response("Invalid token")
}
}
fn show_details(upload: model.Upload, token: String) -> Response {
view.render_detail_html(upload, token)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_details(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
let query = wisp.get_query(req)
let filename = case list.key_find(query, "filename") {
Ok(value) -> value
Error(_) -> ""
}
case service.find_upload(ctx.db, session, filename) {
Ok(upload) -> {
let filepath =
filepath.join(filepath.join(config.upload_dir, session.email), filename)
let token =
service.generate_shared_token(model.Shared(filepath, upload.email))
show_details(upload, token)
}
Error(message) -> error_response(message)
}
}
fn handle_download(req: Request, session: model.Session) -> Response {
let query = wisp.get_query(req)
case list.key_find(query, "token") {
Ok(token) -> {
case service.extract_shared_token(token) {
Ok(shared) -> send_file(shared, session)
Error(message) -> error_response(message)
}
}
Error(_) -> error_response("Invalid token")
}
}
fn handle_share(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
use forms <- wisp.require_form(req)
let query = wisp.get_query(req)
let payload = {
use recepient <- result.try(list.key_find(forms.values, "recepient"))
use filename <- result.try(list.key_find(query, "filename"))
Ok(#(recepient, filename))
}
case payload {
Ok(payload) -> {
case service.share(ctx.db, payload.0, payload.1, session) {
Ok(token) ->
show_share(util.get_base_url(req) <> "/download?token=" <> token)
Error(message) -> error_response(message)
}
}
Error(_) -> wisp.bad_request()
}
}
fn show_error(req: Request) -> Response {
let query = wisp.get_query(req)
let message = case list.key_find(query, "message") {
Ok(value) -> value
Error(_) -> "Unknown error"
}
view.render_error_html(message)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn show_home(req: Request) -> Response {
use <- wisp.require_method(req, Get)
view.index_html
|> string_builder.from_string
|> wisp.html_response(200)
}
fn show_login() -> Response {
view.login_html
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_login(req: Request, ctx: web.Context) -> Response {
use form <- wisp.require_form(req)
let payload = {
use email <- result.try(list.key_find(form.values, "email"))
use password <- result.try(list.key_find(form.values, "password"))
Ok(model.User(email, password))
}
case payload {
Ok(user) -> {
case service.login(ctx.db, user) {
Ok(token) -> create_session(req, token)
Error(message) -> error_response(message)
}
}
Error(_) -> wisp.bad_request()
}
}
fn show_register() -> Response {
view.register_html
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_register(req: Request, ctx: web.Context) -> Response {
use form <- wisp.require_form(req)
let payload = {
use email <- result.try(list.key_find(form.values, "email"))
use password <- result.try(list.key_find(form.values, "password"))
Ok(model.User(email, password))
}
case payload {
Ok(user) -> {
case service.register(ctx.db, user) {
Ok(_) -> wisp.redirect("/login")
Error(message) -> error_response(message)
}
}
Error(_) -> wisp.bad_request()
}
}
fn show_drive(ctx: web.Context, session: model.Session) -> Response {
let drives = service.list_uploads(ctx.db, session)
view.render_drives_html(drives)
|> string_builder.from_string
|> wisp.html_response(200)
}
fn handle_upload(
req: Request,
ctx: web.Context,
session: model.Session,
) -> Response {
use form <- wisp.require_form(req)
let payload = {
use file <- result.try(list.key_find(form.files, "uploaded-file"))
Ok(file)
}
case payload {
Ok(file) -> {
case service.upload(ctx.db, file, session) {
Ok(_) -> wisp.redirect("/drive")
Error(message) -> error_response(message)
}
}
Error(_) -> {
io.debug("Failed to upload file")
wisp.bad_request()
}
}
}
+109
View File
@@ -0,0 +1,109 @@
import app/config
import app/crypto
import app/model
import app/repository
import app/util
import birl
import birl/duration
import filepath
import gleam/result
import simplifile
import sqlight
import wisp
pub fn login(db: sqlight.Connection, user: model.User) -> Result(String, String) {
case repository.login(db, user.email, user.password) {
True -> Ok(generate_session_token(user.email))
False -> Error("Invalid credentials")
}
}
pub fn register(
db: sqlight.Connection,
user: model.User,
) -> Result(String, String) {
let user_dir = filepath.join(config.upload_dir, user.email)
case repository.user_exists(db, user.email) || util.path_exists(user_dir) {
True -> Error("User already exists")
False -> {
result.unwrap(simplifile.create_directory_all(user_dir), Nil)
repository.create_user(db, user.email, user.password)
}
}
}
pub fn list_uploads(
db: sqlight.Connection,
session: model.Session,
) -> List(model.Upload) {
repository.list_uploads(db, session.email)
}
pub fn find_upload(
db: sqlight.Connection,
session: model.Session,
filename: String,
) -> Result(model.Upload, String) {
repository.find_file(db, session.email, filename)
}
pub fn upload(
db: sqlight.Connection,
file: wisp.UploadedFile,
session: model.Session,
) -> Result(String, String) {
let user_dir = filepath.join(config.upload_dir, session.email)
let filepath = filepath.join(user_dir, file.file_name)
let filesize = util.get_file_size(file.path)
case repository.create_upload(db, session.email, file.file_name, filesize) {
Ok(_) -> util.upload_file(file.path, filepath)
Error(_) -> Error("Failed to upload file")
}
}
pub fn share(
db: sqlight.Connection,
recepient: String,
filename: String,
session: model.Session,
) -> Result(String, String) {
let drive = repository.find_file(db, session.email, filename)
let user_dir = filepath.join(config.upload_dir, session.email)
let shared_file = filepath.join(user_dir, filename)
case drive {
Ok(_) -> Ok(generate_shared_token(model.Shared(shared_file, recepient)))
Error(_) -> Error("File not found")
}
}
pub fn extract_session_token(token: String) -> model.Session {
let key = config.aes_key()
case model.session_from_json(crypto.decrypt(key, token)) {
Ok(session) -> session
Error(_) -> model.Session("", 0)
}
}
pub fn extract_shared_token(token: String) -> Result(model.Shared, String) {
let key = config.aes_key()
case model.shared_from_json(crypto.decrypt(key, token)) {
Ok(shared) -> Ok(shared)
Error(_) -> Error("Invalid token")
}
}
pub fn generate_shared_token(data: model.Shared) -> String {
let key = config.aes_key()
crypto.encrypt(key, model.shared_to_json(data))
}
fn generate_session_token(email: String) -> String {
let key = config.aes_key()
crypto.encrypt(
key,
model.session_to_json(model.Session(
email,
birl.to_unix(birl.add(birl.now(), duration.minutes(30))),
)),
)
}
+37
View File
@@ -0,0 +1,37 @@
import app/config
import gleam/http
import gleam/int
import gleam/option
import gleam/result
import simplifile
import wisp
pub fn path_exists(path: String) -> Bool {
result.unwrap(simplifile.is_directory(path), True)
|| result.unwrap(simplifile.is_file(path), True)
|| result.unwrap(simplifile.is_symlink(path), True)
}
pub fn upload_file(src: String, dest: String) -> Result(String, String) {
case !path_exists(dest) && config.max_file_size > get_file_size(src) {
True -> {
result.unwrap(simplifile.copy_file(src, dest), Nil)
Ok("File uploaded")
}
False -> Error("Failed to upload file")
}
}
pub fn get_file_size(path: String) -> Int {
case simplifile.file_info(path) {
Ok(info) -> info.size
Error(_) -> 0
}
}
pub fn get_base_url(req: wisp.Request) -> String {
let scheme = http.scheme_to_string(req.scheme)
let host = req.host
let port = int.to_string(option.unwrap(req.port, 80))
scheme <> "://" <> host <> ":" <> port
}
+603
View File
@@ -0,0 +1,603 @@
import app/model
import gleam/int
import gleam/list
import gleam/string
import gleam/string_builder
pub const index_html = "<!doctype html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>gleam-drive</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<div class='flex flex-col min-h-screen'>
<div class='px-4 lg:px-6 h-14 flex items-center'>
<div class='flex items-center justify-center' href='#'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='round'
stroke-linejoin='round'
class='h-6 w-6'
>
<path fill-rule='evenodd' clip-rule='evenodd' d='M12 1.25C11.3953 1.25 10.8384 1.40029 10.2288 1.65242C9.64008 1.89588 8.95633 2.25471 8.1049 2.70153L6.03739 3.78651C4.99242 4.33487 4.15616 4.77371 3.51047 5.20491C2.84154 5.65164 2.32632 6.12201 1.95112 6.75918C1.57718 7.39421 1.40896 8.08184 1.32829 8.90072C1.24999 9.69558 1.24999 10.6731 1.25 11.9026V12.0974C1.24999 13.3268 1.24999 14.3044 1.32829 15.0993C1.40896 15.9182 1.57718 16.6058 1.95112 17.2408C2.32632 17.878 2.84154Line truncated
</svg>
<span class='sr-only'>kode-viewer</span>
</div>
</div>
<main class='flex-1 flex flex-col items-center justify-center space-y-4 text-center'>
<div class='space-y-2'>
<h1 class='text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none'>gleam-drive</h1>
<p class='max-w-[600px] text-muted-foreground text-gray-500 md:text-xl'>
Elevate your file-share experience with gleam-drive.
</p>
</div>
<a class='flex justify-center' href='/login'>
<button class='rounded-md text-sm font-medium bg-background hover:bg-gray-200 text-black font-bold py-2 px-4 border border-accent rounded'>
Get Started
</button>
</a>
</main>
</div>
</html>"
pub const login_html = "<!doctype html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>gleam-drive</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<div class='flex flex-col min-h-screen'>
<div class='px-4 lg:px-6 h-14 flex items-center'>
<div class='flex items-center justify-center' href='#'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='round'
stroke-linejoin='round'
class='h-6 w-6'
>
<path fill-rule='evenodd' clip-rule='evenodd' d='M12 1.25C11.3953 1.25 10.8384 1.40029 10.2288 1.65242C9.64008 1.89588 8.95633 2.25471 8.1049 2.70153L6.03739 3.78651C4.99242 4.33487 4.15616 4.77371 3.51047 5.20491C2.84154 5.65164 2.32632 6.12201 1.95112 6.75918C1.57718 7.39421 1.40896 8.08184 1.32829 8.90072C1.24999 9.69558 1.24999 10.6731 1.25 11.9026V12.0974C1.24999 13.3268 1.24999 14.3044 1.32829 15.0993C1.40896 15.9182 1.57718 16.6058 1.95112 17.2408C2.32632 17.878 2.84154Line truncated
</svg>
<span class='sr-only'>gleam-drive</span>
</div>
</div>
<main class='flex-1 flex flex-col items-center justify-center text-center'>
<div class='w-full max-w-md space-y-8'>
<div>
<h2 class='mt-6 text-center text-3xl font-bold tracking-tight text-foreground'>Sign in to Gleam Drive</h2>
</div>
<form class='space-y-4' action='/login' method='POST'>
<div>
<input
class='h-10 bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 relative block w-full appearance-none rounded-md border border-muted px-3 py-2 text-foreground placeholder-muted-foreground focus:z-10 focus:border-primary focus:outline-none focus:ring-primary sm:text-sm'
type='email'
id='email'
autocomplete='email'
placeholder='Email'
name='email'
required
/>
</div>
<div>
<label
class='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sr-only'
for='password'
>
Password
</label>
<input
class='h-10 bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 relative block w-full appearance-none rounded-md border border-muted px-3 py-2 text-foreground placeholder-muted-foreground focus:z-10 focus:border-primary focus:outline-none focus:ring-primary sm:text-sm'
type='password'
id='password'
autocomplete='current-password'
required
placeholder='Password'
name='password'
/>
</div>
<div>
<button type='submit' class='rounded-md text-sm w-full bg-background hover:bg-gray-200 text-black font-bold py-2 px-4 border border-accent rounded'>
Sign in
</button>
</div>
<div class='text-center'>
<a class='text-sm font-medium text-muted-foreground text-gray-500 hover:text-gray-900' href='/register'>
Don't have an account? Register
</a>
</div>
</form>
</div>
</main>
</div>
</html>"
pub const register_html = "<!doctype html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>gleam-drive</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<div class='flex flex-col min-h-screen'>
<div class='px-4 lg:px-6 h-14 flex items-center'>
<div class='flex items-center justify-center' href='#'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='round'
stroke-linejoin='round'
class='h-6 w-6'
>
<path fill-rule='evenodd' clip-rule='evenodd' d='M12 1.25C11.3953 1.25 10.8384 1.40029 10.2288 1.65242C9.64008 1.89588 8.95633 2.25471 8.1049 2.70153L6.03739 3.78651C4.99242 4.33487 4.15616 4.77371 3.51047 5.20491C2.84154 5.65164 2.32632 6.12201 1.95112 6.75918C1.57718 7.39421 1.40896 8.08184 1.32829 8.90072C1.24999 9.69558 1.24999 10.6731 1.25 11.9026V12.0974C1.24999 13.3268 1.24999 14.3044 1.32829 15.0993C1.40896 15.9182 1.57718 16.6058 1.95112 17.2408C2.32632 17.878 2.84154Line truncated
</svg>
<span class='sr-only'>gleam-drive</span>
</div>
</div>
<main class='flex-1 flex flex-col items-center justify-center text-center'>
<div class='w-full max-w-md space-y-8'>
<div>
<h2 class='mt-6 text-center text-3xl font-bold tracking-tight text-foreground'>Register to Gleam Drive</h2>
</div>
<form class='space-y-4' action='/register' method='POST'>
<div>
<label
class='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sr-only'
for='email'
>
Email
</label>
<input
class='h-10 bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 relative block w-full appearance-none rounded-md border border-muted px-3 py-2 text-foreground placeholder-muted-foreground focus:z-10 focus:border-primary focus:outline-none focLine truncated
type='email'
id='email'
autocomplete='email'
required
placeholder='Email'
name='email'
/>
</div>
<div>
<label
class='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sr-only'
for='password'
>
Password
</label>
<input
class='h-10 bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 relative block w-full appearance-none rounded-md border border-muted px-3 py-2 text-foreground placeholder-muted-foreground focus:z-10 focus:border-primary focus:outline-none focLine truncated
type='password'
id='password'
autocomplete='current-password'
required
placeholder='Password'
name='password'
/>
</div>
<div>
<button type='submit' class='rounded-md text-sm w-full bg-background hover:bg-gray-200 text-black font-bold py-2 px-4 border border-accent rounded'>
Register
</button>
</div>
<div class='text-center'>
<a class='text-sm font-medium text-muted-foreground text-gray-500 hover:text-gray-900' href='/login'>
Already have an account? Sign in
</a>
</div>
</form>
</div>
</main>
</div>
</html>"
pub const drive_html = "<!doctype html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>gleam-drive</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<div class='flex flex-col h-full w-full'>
<div class='bg-background border-b flex px-4 sm:px-6 h-14'>
<div class='flex flex-row justify-between items-center w-full'>
<a class='flex items-center' href='/drive'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='round'
stroke-linejoin='round'
class='h-6 w-6'
>
<path fill-rule='evenodd' clip-rule='evenodd' d='M12 1.25C11.3953 1.25 10.8384 1.40029 10.2288 1.65242C9.64008 1.89588 8.95633 2.25471 8.1049 2.70153L6.03739 3.78651C4.99242 4.33487 4.15616 4.77371 3.51047 5.20491C2.84154 5.65164 2.32632 6.12201 1.95112 6.75918C1.57718 7.39421 1.40896 8.08184 1.32829 8.90072C1.24999 9.69558 1.24999 10.6731 1.25 11.9026V12.0974C1.24999 13.3268 1.24999 14.3044 1.32829 15.0993C1.40896 15.9182 1.57718 16.6058 1.95112 17.2408C2.32632 17.878 2.84154 1Line truncated
</svg>
<span class='sr-only'>gleam-drive</span>
</a>
<div class='flex'>
<a href='/logout'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='2'
stroke-linecap='round'
stroke-linejoin='round'
class='h-5 w-5'
>
<path d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4' />
<polyline points='16 17 21 12 16 7' />
<line x1='21' x2='9' y1='12' y2='12' />
</svg>
</a>
</div>
</div>
</div>
<main class='flex flex-col items-center h-full w-full my-5 gap-4'>
<div class='space-y-6 w-1/2'>
<div class='flex items-center justify-between'>
<div class='space-y-1'>
<h2 class='text-2xl font-bold'>File Upload</h2>
<p class='text-muted-foreground'>Drag and drop files or click to upload.</p>
</div>
<form method=post action='/drive' enctype='multipart/form-data' class='flex flex-row gap-2'>
<input class='block my-2 text-sm focus:outline-none' id='uploaded-file' name='uploaded-file' type='file'>
<button type='submit' class='inline-flex items-center justify-center whitespace-nowrap rounded-md text-md text-black font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-gray-200 hover:text-accent-foreground h-10 px-4 py-2'>
Upload File
</button>
</form>
</div>
<div>
FILES_LIST
</div>
</div>
</main>
</div>
</html>"
pub const share_html = "<!doctype html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>gleam-drive</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<div class='flex flex-col h-full w-full'>
<div class='bg-background border-b flex px-4 sm:px-6 h-14'>
<div class='flex flex-row justify-between items-center w-full'>
<a class='flex items-center' href='/drive'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='round'
stroke-linejoin='round'
class='h-6 w-6'
>
<path fill-rule='evenodd' clip-rule='evenodd' d='M12 1.25C11.3953 1.25 10.8384 1.40029 10.2288 1.65242C9.64008 1.89588 8.95633 2.25471 8.1049 2.70153L6.03739 3.78651C4.99242 4.33487 4.15616 4.77371 3.51047 5.20491C2.84154 5.65164 2.32632 6.12201 1.95112 6.75918C1.57718 7.39421 1.40896 8.08184 1.32829 8.90072C1.24999 9.69558 1.24999 10.6731 1.25 11.9026V12.0974C1.24999 13.3268 1.24999 14.3044 1.32829 15.0993C1.40896 15.9182 1.57718 16.6058 1.95112 17.2408C2.32632 17.878 2.84154 1Line truncated
</svg>
<span class='sr-only'>gleam-drive</span>
</a>
<div class='flex'>
<a href='/logout'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='2'
stroke-linecap='round'
stroke-linejoin='round'
class='h-5 w-5'
>
<path d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4' />
<polyline points='16 17 21 12 16 7' />
<line x1='21' x2='9' y1='12' y2='12' />
</svg>
</a>
</div>
</div>
</div>
<main class='flex flex-col items-center h-full w-full my-5'>
<div class='space-y-6 w-1/2'>
<div class='flex items-center justify-between flex-col gap-4'>
<div class='flex items-center justify-between mb-4'>
<div>
<h1 class='text-2xl font-bold'>Copy this link</h1>
</div>
</div>
<div class='items-center flex justify-end gap-2'>
<pre class='bg-gray-200 text-xs text-muted-foreground p-2 rounded-md'>DOWNLOAD_LINK</pre>
</div>
</div>
</div>
</main>
</div>
</html>"
pub const error_html = "<!doctype html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>gleam-drive</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<div class='flex flex-col min-h-screen'>
<div class='px-4 lg:px-6 h-14 flex items-center'>
<div class='flex items-center justify-center' href='#'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='round'
stroke-linejoin='round'
class='h-6 w-6'
>
<path fill-rule='evenodd' clip-rule='evenodd' d='M12 1.25C11.3953 1.25 10.8384 1.40029 10.2288 1.65242C9.64008 1.89588 8.95633 2.25471 8.1049 2.70153L6.03739 3.78651C4.99242 4.33487 4.15616 4.77371 3.51047 5.20491C2.84154 5.65164 2.32632 6.12201 1.95112 6.75918C1.57718 7.39421 1.40896 8.08184 1.32829 8.90072C1.24999 9.69558 1.24999 10.6731 1.25 11.9026V12.0974C1.24999 13.3268 1.24999 14.3044 1.32829 15.0993C1.40896 15.9182 1.57718 16.6058 1.95112 17.2408C2.32632 17.878 2.84154Line truncated
</svg>
<span class='sr-only'>gleam-drive</span>
</div>
</div>
<main class='flex-1 flex flex-col items-center justify-center text-center'>
<div class='w-full max-w-md space-y-8'>
<div>
<h2 class='mt-6 text-center text-3xl font-bold tracking-tight text-foreground'>Oops</h2>
</div>
<div>
<p class='text-center text-foreground'>ERROR_MESSAGE</p>
</div>
<div>
<a href='/drive' class='rounded-md text-sm w-full bg-background hover:bg-gray-200 text-black font-bold py-2 px-4 border border-green-700 rounded'>
Go back
</a>
</div>
</div>
</main>
</div>
</html>"
pub const detail_html = "<!doctype html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>gleam-drive</title>
<script src='https://cdn.tailwindcss.com'></script>
</head>
<div class='flex flex-col h-full w-full'>
<div class='bg-background border-b flex px-4 sm:px-6 h-14'>
<div class='flex flex-row justify-between items-center w-full'>
<a class='flex items-center' href='/drive'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='1'
stroke-linecap='round'
stroke-linejoin='round'
class='h-6 w-6'
>
<path fill-rule='evenodd' clip-rule='evenodd' d='M12 1.25C11.3953 1.25 10.8384 1.40029 10.2288 1.65242C9.64008 1.89588 8.95633 2.25471 8.1049 2.70153L6.03739 3.78651C4.99242 4.33487 4.15616 4.77371 3.51047 5.20491C2.84154 5.65164 2.32632 6.12201 1.95112 6.75918C1.57718 7.39421 1.40896 8.08184 1.32829 8.90072C1.24999 9.69558 1.24999 10.6731 1.25 11.9026V12.0974C1.24999 13.3268 1.24999 14.3044 1.32829 15.0993C1.40896 15.9182 1.57718 16.6058 1.95112 17.2408C2.32632 17.878 2.84154 1Line truncated
</svg>
<span class='sr-only'>gleam-drive</span>
</a>
<div class='flex'>
<a href='/logout'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='2'
stroke-linecap='round'
stroke-linejoin='round'
class='h-5 w-5'
>
<path d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4' />
<polyline points='16 17 21 12 16 7' />
<line x1='21' x2='9' y1='12' y2='12' />
</svg>
</a>
</div>
</div>
</div>
<main class='flex flex-col items-center h-full w-full my-5'>
<div class='space-y-6 w-1/2'>
<div class='flex items-center justify-between flex-col gap-4'>
<div class='flex items-center justify-between mb-4'>
<div>
<h1 class='text-2xl font-bold text-center'>FILENAME</h1>
<div class='space-y-4'>
<div class='bg-muted rounded-md p-4'>
<div class='grid grid-cols-2 gap-4'>
<div>
<p class='font-bold text-md'>File Size</p>
<p class='text-muted-foreground text-sm'>FILESIZE KB</p>
</div>
<div>
<p class='font-bold text-md'>Last Modified</p>
<p class='text-muted-foreground text-sm'>CREATED_AT</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class='items-center flex justify-end gap-2'>
<form class='flex h-10 w-full' action='/share?filename=FILENAME' method='POST'>
<div class='flex flex-row justify-center items-center h h-full w-full gap-4'>
<input
class='flex w-5/6 bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 w-full rounded-lg px-4 py-2 border border-accent focus:border-primary focus:ring-primary'
placeholder='Recepient ...'
type='email'
id='recepient'
name='recepient''
required
/>
<button type='submit' class='h-10 rounded-md text-md px-5 bg-background hover:bg-gray-200 font-bold border border-accent rounded'>
Share
</button>
</div>
</form>
</div>
<div class='items-center flex justify-end gap-2 w-1/2 px-20'>
<a href='download?token=DOWNLOAD_LINK' class='h-10 w-full rounded-md text-md px-3 bg-background hover:bg-gray-200 font-bold border border-accent rounded'>
<p class='flex h-full items-center justify-center'>Download</p>
</a>
</div>
</div>
</div>
</main>
</div>
</html>"
pub const no_files_html = "
<div class='flex flex-col items-center justify-center gap-4'>
<h1 class='text-muted-foreground text-gray-500 md:text-xl'>No files found</h1>
</div>
"
pub const some_files_html = "
<div class='divide-y rounded-md border'>
FILES
</div>
"
pub const file_html = "
<div class='flex items-center justify-between gap-4 p-4'>
<div class='flex items-center gap-4'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='2'
stroke-linecap='round'
stroke-linejoin='round'
class='h-8 w-8 text-muted-foreground'
>
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'></path>
<path d='M14 2v4a2 2 0 0 0 2 2h4'></path>
</svg>
<div>
<p class='font-medium'>FILENAME</p>
<p class='text-sm text-muted-foreground'>FILESIZE KB</p>
</div>
</div>
<div class='flex items-center gap-2'>
<a href='/details?filename=FILENAME' class='inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-gray-200 hover:text-accent-foreground h-10 w-10'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
stroke-width='2'
stroke-linecap='round'
stroke-linejoin='round'
class='h-5 w-5'
>
<path d='M15.7955 15.8111L21 21M18 10.5C18 14.6421 14.6421 18 10.5 18C6.35786 18 3 14.6421 3 10.5C3 6.35786 6.35786 3 10.5 3C14.6421 3 18 6.35786 18 10.5Z' stroke='#000000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'></path>
</svg>
<span class='sr-only'>Details</span>
</a>
</div>
</div>
"
pub fn render_error_html(message: String) -> String {
string.replace(error_html, "ERROR_MESSAGE", message)
}
pub fn render_files_html(files: List(model.Upload)) -> String {
list.map(files, fn(file) {
string.replace(
some_files_html,
"FILES",
string.replace(
string.replace(file_html, "FILENAME", file.filename),
"FILESIZE",
int.to_string(file.filesize / 1024),
),
)
})
|> string_builder.from_strings
|> string_builder.to_string
}
pub fn render_drives_html(files: List(model.Upload)) -> String {
case list.length(files) {
0 -> string.replace(drive_html, "FILES_LIST", no_files_html)
_ -> string.replace(drive_html, "FILES_LIST", render_files_html(files))
}
}
pub fn render_detail_html(file: model.Upload, download_link: String) -> String {
string.replace(
string.replace(
string.replace(
string.replace(detail_html, "FILENAME", file.filename),
"FILESIZE",
int.to_string(file.filesize / 1024),
),
"CREATED_AT",
file.timestamp,
),
"DOWNLOAD_LINK",
download_link,
)
}
pub fn render_share_html(download_link: String) -> String {
string.replace(share_html, "DOWNLOAD_LINK", download_link)
}
+17
View File
@@ -0,0 +1,17 @@
import app/config
import sqlight
import wisp
pub type Context {
Context(db: sqlight.Connection)
}
pub fn middleware(
req: wisp.Request,
handle_request: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
use <- wisp.log_request(req)
use <- wisp.rescue_crashes
use req <- wisp.handle_head(req)
handle_request(req |> wisp.set_max_files_size(config.max_file_size))
}
+11
View File
@@ -0,0 +1,11 @@
-module(crypto_ffi).
-export([encrypt/2, decrypt/2, hash/1]).
encrypt(Key, Data) ->
crypto:crypto_one_time(aes_128_ecb, Key, Data, true).
decrypt(Key, Data) ->
crypto:crypto_one_time(aes_128_ecb, Key, Data, false).
hash(Data) ->
binary:encode_hex(crypto:hash(sha256, Data), uppercase).
+23
View File
@@ -0,0 +1,23 @@
import app/repository
import app/router
import app/web
import gleam/erlang/process
import mist
import sqlight
import wisp
pub fn main() {
wisp.configure_logger()
let secret_key_base = wisp.random_string(64)
use db <- sqlight.with_connection("gdrive.db")
let assert Ok(Nil) = repository.init(db)
let context = web.Context(db: db)
let handler = router.handle_request(_, context)
let assert Ok(_) =
wisp.mist_handler(handler, secret_key_base)
|> mist.new
|> mist.port(8000)
|> mist.start_https("cert.pem", "key.pem")
process.sleep_forever()
}