Compare commits
No commits in common. "develop" and "develop" have entirely different histories.
|
@ -1,6 +1,5 @@
|
||||||
*
|
*
|
||||||
|
|
||||||
!.cargo/
|
|
||||||
!Cargo.lock
|
!Cargo.lock
|
||||||
!Cargo.toml
|
!Cargo.toml
|
||||||
!Makefile
|
!Makefile
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = false
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
41
API.md
41
API.md
|
@ -1,41 +0,0 @@
|
||||||
# API Design
|
|
||||||
|
|
||||||
This file describes the API that the software adheres to. All routes are defined under a shared `api` namespace.
|
|
||||||
|
|
||||||
`(A)` means the route can only be accessed by an admin user.
|
|
||||||
|
|
||||||
## v1
|
|
||||||
|
|
||||||
## Authentification
|
|
||||||
|
|
||||||
* POST `/auth/login` - generate new JWT & refresh token pair given user credentials
|
|
||||||
* POST `/auth/refresh` - generate new JWT & refresh token pair given valid refresh token
|
|
||||||
|
|
||||||
## Posts
|
|
||||||
|
|
||||||
* GET `/posts?<offset>&<limit>` - get list of posts from the default feed given offset & limit
|
|
||||||
* GET `/posts?<section_id_or_shortname>&<offset>&<limit>` - get list of posts of a specific section
|
|
||||||
* (A) POST `/posts` - create a new post
|
|
||||||
* GET `/posts/<id>` - get a specific post
|
|
||||||
* (A) DELETE `/posts/<id>` - delete a post
|
|
||||||
* (A) PATCH `/posts/<id>` - patch a post
|
|
||||||
|
|
||||||
## Sections
|
|
||||||
|
|
||||||
* GET `/sections?<offset>&<limit>` - get list of sections
|
|
||||||
* GET `/sections/<id_or_shortname>` - get specific section
|
|
||||||
* (A) POST `/sections` - create a new section
|
|
||||||
* (A) PATCH `/sections/<id_or_shortname>` - patch a section
|
|
||||||
* (A) DELETE `/sections/<id_or_shortname>` - delete a section (what happens with posts?)
|
|
||||||
|
|
||||||
## Users
|
|
||||||
|
|
||||||
* (A) GET `/users?<offset>&<limit>`
|
|
||||||
* (A) POST `/users`
|
|
||||||
* (A) GET `/users/<id_or_username>`
|
|
||||||
* (A) PATCH `/users/<id_or_username>`
|
|
||||||
* (A) DELETE `/users/<id_or_username>`
|
|
||||||
|
|
||||||
## Feeds
|
|
||||||
|
|
||||||
WIP
|
|
|
@ -1109,7 +1109,6 @@ dependencies = [
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"ubyte",
|
"ubyte",
|
||||||
"uuid",
|
|
||||||
"version_check",
|
"version_check",
|
||||||
"yansi",
|
"yansi",
|
||||||
]
|
]
|
||||||
|
@ -1156,7 +1155,6 @@ dependencies = [
|
||||||
"time 0.2.27",
|
"time 0.2.27",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uncased",
|
"uncased",
|
||||||
"uuid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -8,16 +8,11 @@ edition = "2018"
|
||||||
name = "rbd"
|
name = "rbd"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
|
||||||
web = []
|
|
||||||
docs = []
|
|
||||||
static = ["web", "docs"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Backend web framework
|
# Backend web framework
|
||||||
rocket = { version = "0.5.0-rc.1", features = [ "json", "uuid" ] }
|
rocket = { version = "0.5.0-rc.1", features = [ "json" ] }
|
||||||
# Used to provide Rocket routes with database connections
|
# Used to provide Rocket routes with database connections
|
||||||
rocket_sync_db_pools = { version = "0.1.0-rc.1", default_features = false, features = [ "diesel_postgres_pool" ] }
|
rocket_sync_db_pools = { version = "0.1.0-rc.1", default_features = false, features = [ "diesel_postgres_pool" ] }
|
||||||
# Used to (de)serialize JSON
|
# Used to (de)serialize JSON
|
||||||
|
|
81
Dockerfile
81
Dockerfile
|
@ -1,66 +1,19 @@
|
||||||
# Build frontend files
|
FROM rust:1.54
|
||||||
FROM node:16 AS fbuilder
|
|
||||||
|
ENV PREFIX="/usr/src/out/prefix" \
|
||||||
|
CC="musl-gcc -fPIC -pie -static" \
|
||||||
|
LD_LIBRARY_PATH="$PREFIX" \
|
||||||
|
PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" \
|
||||||
|
PATH="/usr/local/bin:/root/.cargo/bin:$PATH"
|
||||||
|
|
||||||
|
RUN apt update && \
|
||||||
|
apt install -y --no-install-recommends \
|
||||||
|
musl-dev \
|
||||||
|
musl-tools \
|
||||||
|
libpq-dev \
|
||||||
|
libssl-dev && \
|
||||||
|
rustup target add x86_64-unknown-linux-musl && \
|
||||||
|
mkdir "$PREFIX" && \
|
||||||
|
echo "$PREFIX/lib" >> /etc/ld-musl-x86_64.path
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY web/ ./
|
|
||||||
|
|
||||||
RUN yarn install && \
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
|
|
||||||
# Build backend & backend docs
|
|
||||||
FROM rust:1.55-alpine AS builder
|
|
||||||
|
|
||||||
ARG DI_VER=1.2.5
|
|
||||||
|
|
||||||
# ENV OPENSSL_STATIC=1 \
|
|
||||||
# PQ_LIB_STATIC=1
|
|
||||||
|
|
||||||
RUN apk update && \
|
|
||||||
apk add --no-cache \
|
|
||||||
postgresql \
|
|
||||||
postgresql-dev \
|
|
||||||
openssl-dev \
|
|
||||||
build-base
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# Build backend
|
|
||||||
COPY .cargo/ ./.cargo
|
|
||||||
COPY src/ ./src
|
|
||||||
COPY migrations/ ./migrations
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
|
|
||||||
RUN cargo build --release && \
|
|
||||||
cargo doc --no-deps
|
|
||||||
|
|
||||||
# Build dumb-init
|
|
||||||
RUN curl -sSL "https://github.com/Yelp/dumb-init/archive/refs/tags/v$DI_VER.tar.gz" | \
|
|
||||||
tar -xzf - && \
|
|
||||||
cd "dumb-init-$DI_VER" && \
|
|
||||||
make build && \
|
|
||||||
mv dumb-init ..
|
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.14.2
|
|
||||||
|
|
||||||
RUN mkdir -p /var/www/html
|
|
||||||
|
|
||||||
COPY --from=fbuilder /usr/src/app/dist /var/www/html/site
|
|
||||||
COPY --from=builder /usr/src/app/out/target/doc /var/www/html/doc
|
|
||||||
COPY --from=builder /usr/src/app/out/target/release/rbd /usr/bin/rbd
|
|
||||||
COPY --from=builder /usr/src/app/dumb-init /usr/bin/dumb-init
|
|
||||||
|
|
||||||
ENTRYPOINT [ "dumb-init", "--" ]
|
|
||||||
CMD [ "/usr/bin/rbd" ]
|
|
||||||
|
|
||||||
# RUN apt update && \
|
|
||||||
# apt install -y --no-install-recommends \
|
|
||||||
# musl-dev \
|
|
||||||
# musl-tools \
|
|
||||||
# libpq-dev \
|
|
||||||
# libssl-dev && \
|
|
||||||
# rustup target add x86_64-unknown-linux-musl && \
|
|
||||||
# mkdir "$PREFIX" && \
|
|
||||||
# echo "$PREFIX/lib" >> /etc/ld-musl-x86_64.path
|
|
||||||
|
|
34
ROADMAP.md
34
ROADMAP.md
|
@ -1,34 +0,0 @@
|
||||||
# Roadmap
|
|
||||||
|
|
||||||
This file describes a general plan for the software, divided into versions.
|
|
||||||
|
|
||||||
## v0.1.0
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
|
|
||||||
* Version 1 of backend API
|
|
||||||
* Read-only frontend (no login)
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Version 0.1.0 will be the first deployable version. The goal is to replace my
|
|
||||||
current blog with an instance of v0.1.0. This includes developing a (basic) SDK
|
|
||||||
(probably in Python) that allows me to interact with my instance, or rather
|
|
||||||
just post stuff.
|
|
||||||
|
|
||||||
## v1.0.0
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
|
|
||||||
* First stable release
|
|
||||||
* Base for all other releases
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
For me, a 1.0 release indicates that the project is stable and can be actively
|
|
||||||
and efficiently worked on. I basically just want to iron out any wrinkles from
|
|
||||||
the 0.1 release, so that I have a solid base to develop all other features on.
|
|
||||||
This will also allow me to better combine the development of this project with
|
|
||||||
my studies, as it can be properly planned and managed whenever I have the time.
|
|
||||||
Any other features won't appear in this file. Rather, they will be managed
|
|
||||||
using the milestones & issues on my Gitea instance.
|
|
12
Rb.yaml
12
Rb.yaml
|
@ -23,21 +23,13 @@ debug:
|
||||||
url: "postgres://rb:rb@localhost:5432/rb"
|
url: "postgres://rb:rb@localhost:5432/rb"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
keep_alive: 5
|
|
||||||
read_timeout: 5
|
|
||||||
write_timeout: 5
|
|
||||||
log_level: "normal"
|
|
||||||
limits:
|
|
||||||
forms: 32768
|
|
||||||
|
|
||||||
admin_user: "admin"
|
admin_user: "admin"
|
||||||
admin_pass: "password"
|
admin_pass: "password"
|
||||||
jwt:
|
jwt:
|
||||||
key: "secret"
|
key: "secret"
|
||||||
refresh_token_size: 64
|
refresh_token_size: 64
|
||||||
# Just 5 seconds for debugging
|
refresh_token_expire: 86400
|
||||||
refresh_token_expire: 60
|
|
||||||
|
|
||||||
databases:
|
databases:
|
||||||
postgres_rb:
|
postgres_rb:
|
||||||
url: "postgres://rb:rb@localhost:5432/rb"
|
url: "postgres://rb:rb@db:5432/rb"
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
# apt update
|
||||||
|
# apt install \
|
||||||
|
# -y --no-install-recommends \
|
||||||
|
# musl-dev \
|
||||||
|
# musl-tools \
|
||||||
|
# libssl-dev \
|
||||||
|
# libpq-dev
|
||||||
|
|
||||||
|
make
|
|
@ -1,7 +0,0 @@
|
||||||
-- This file should undo anything in `up.sql`
|
|
||||||
drop trigger insert_enforce_post_titles on posts;
|
|
||||||
drop trigger update_enforce_post_titles on posts;
|
|
||||||
drop function enforce_post_titles;
|
|
||||||
|
|
||||||
drop table posts cascade;
|
|
||||||
drop table sections cascade;
|
|
|
@ -1,58 +0,0 @@
|
||||||
-- Your SQL goes here
|
|
||||||
create table sections (
|
|
||||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
||||||
|
|
||||||
-- Title of the section
|
|
||||||
title varchar(255) UNIQUE NOT NULL,
|
|
||||||
-- Name to use when routing (this just makes for prettier URLs)
|
|
||||||
shortname varchar(32) UNIQUE NOT NULL,
|
|
||||||
-- Optional description of the section
|
|
||||||
description text,
|
|
||||||
-- Wether to show the section in the default list on the homepage
|
|
||||||
is_default boolean NOT NULL DEFAULT false,
|
|
||||||
-- Wether the posts should contain titles or not
|
|
||||||
has_titles boolean NOT NULL DEFAULT true
|
|
||||||
);
|
|
||||||
|
|
||||||
create table posts (
|
|
||||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
||||||
|
|
||||||
section_id uuid NOT NULL REFERENCES sections(id) ON DELETE CASCADE,
|
|
||||||
-- Title of the post
|
|
||||||
-- Wether this is NULL or not is enforced using the enforce_post_titles trigger
|
|
||||||
title varchar(255),
|
|
||||||
-- Post date, defaults to today
|
|
||||||
publish_date date NOT NULL DEFAULT now(),
|
|
||||||
-- Content of the post
|
|
||||||
content text NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
create function enforce_post_titles() returns trigger as $enforce_post_titles$
|
|
||||||
begin
|
|
||||||
-- Check for a wrongfully null title
|
|
||||||
if new.title is null and exists (
|
|
||||||
select 1 from sections where id = new.section_id and has_titles
|
|
||||||
) then
|
|
||||||
raise exception 'Expected a post title, but got null.';
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if new.title is not null and exists (
|
|
||||||
select 1 from sections where id = new.section_id and not has_titles
|
|
||||||
) then
|
|
||||||
raise exception 'Expected an empty post title, but got a value.';
|
|
||||||
end if;
|
|
||||||
|
|
||||||
return new;
|
|
||||||
end;
|
|
||||||
$enforce_post_titles$ language plpgsql;
|
|
||||||
|
|
||||||
create trigger insert_enforce_post_titles
|
|
||||||
before insert on posts
|
|
||||||
for each row
|
|
||||||
execute function enforce_post_titles();
|
|
||||||
|
|
||||||
create trigger update_enforce_post_titles
|
|
||||||
before update of title on posts
|
|
||||||
for each row
|
|
||||||
when (old.title is distinct from new.title)
|
|
||||||
execute function enforce_post_titles();
|
|
|
@ -36,7 +36,7 @@ license_template_path = ""
|
||||||
make_backup = false
|
make_backup = false
|
||||||
match_arm_blocks = true
|
match_arm_blocks = true
|
||||||
match_arm_leading_pipes = "Never"
|
match_arm_leading_pipes = "Never"
|
||||||
match_block_trailing_comma = true
|
match_block_trailing_comma = false
|
||||||
max_width = 100
|
max_width = 100
|
||||||
merge_derives = true
|
merge_derives = true
|
||||||
newline_style = "Auto"
|
newline_style = "Auto"
|
||||||
|
|
17
src/admin.rs
17
src/admin.rs
|
@ -10,11 +10,11 @@ use crate::{
|
||||||
RbDbConn,
|
RbDbConn,
|
||||||
};
|
};
|
||||||
|
|
||||||
// #[get("/users")]
|
#[get("/users")]
|
||||||
// pub async fn get_users(_admin: Admin, conn: RbDbConn) -> RbResult<Json<Vec<db::User>>>
|
pub async fn get_users(_admin: Admin, conn: RbDbConn) -> RbResult<Json<Vec<db::User>>>
|
||||||
// {
|
{
|
||||||
// Ok(Json(conn.run(|c| db::users::all(c)).await?))
|
Ok(Json(conn.run(|c| db::users::all(c)).await?))
|
||||||
// }
|
}
|
||||||
|
|
||||||
#[post("/users", data = "<user>")]
|
#[post("/users", data = "<user>")]
|
||||||
pub async fn create_user(_admin: Admin, conn: RbDbConn, user: Json<db::NewUser>) -> RbResult<()>
|
pub async fn create_user(_admin: Admin, conn: RbDbConn, user: Json<db::NewUser>) -> RbResult<()>
|
||||||
|
@ -48,11 +48,8 @@ pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) ->
|
||||||
admin: true,
|
admin: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if db::users::find_by_username(conn, username).is_ok() {
|
db::users::create_or_update(conn, &new_user)
|
||||||
db::users::create(conn, &new_user);
|
.map_err(|_| RbError::Custom("Couldn't create admin."))?;
|
||||||
}
|
|
||||||
// db::users::create_or_update(conn, &new_user)
|
|
||||||
// .map_err(|_| RbError::Custom("Couldn't create admin."))?;
|
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
//! The db module contains all Diesel-related logic. This is to prevent the various Diesel imports
|
|
||||||
//! from poluting other modules' namespaces.
|
|
||||||
|
|
||||||
pub mod posts;
|
|
||||||
pub mod sections;
|
|
||||||
pub mod tokens;
|
pub mod tokens;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
||||||
pub use posts::{NewPost, PatchPost, Post};
|
|
||||||
pub use sections::{NewSection, Section};
|
|
||||||
pub use tokens::{NewRefreshToken, RefreshToken};
|
pub use tokens::{NewRefreshToken, RefreshToken};
|
||||||
pub use users::{NewUser, User};
|
pub use users::{NewUser, User};
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
use chrono::NaiveDate;
|
|
||||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
errors::{RbError, RbOption, RbResult},
|
|
||||||
schema::{posts, posts::dsl::*},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Queryable, Serialize)]
|
|
||||||
pub struct Post
|
|
||||||
{
|
|
||||||
pub id: Uuid,
|
|
||||||
pub section_id: Uuid,
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub publish_date: NaiveDate,
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Insertable)]
|
|
||||||
#[table_name = "posts"]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct NewPost
|
|
||||||
{
|
|
||||||
pub section_id: Uuid,
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub publish_date: NaiveDate,
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, AsChangeset)]
|
|
||||||
#[table_name = "posts"]
|
|
||||||
pub struct PatchPost
|
|
||||||
{
|
|
||||||
pub section_id: Option<Uuid>,
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub publish_date: Option<NaiveDate>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Post>>
|
|
||||||
{
|
|
||||||
Ok(posts
|
|
||||||
.offset(offset_.into())
|
|
||||||
.limit(limit_.into())
|
|
||||||
.load(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't query posts."))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find(conn: &PgConnection, id_: &Uuid) -> RbOption<Post>
|
|
||||||
{
|
|
||||||
match posts.find(id_).first(conn) {
|
|
||||||
Ok(val) => Ok(Some(val)),
|
|
||||||
Err(diesel::NotFound) => Ok(None),
|
|
||||||
_ => Err(RbError::DbError("Couldn't find post.")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<Post>
|
|
||||||
{
|
|
||||||
Ok(insert_into(posts)
|
|
||||||
.values(new_post)
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't insert post."))?)
|
|
||||||
|
|
||||||
// TODO check for conflict?
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchPost) -> RbResult<Post>
|
|
||||||
{
|
|
||||||
Ok(diesel::update(posts.filter(id.eq(post_id)))
|
|
||||||
.set(patch_post)
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't update post."))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
|
||||||
{
|
|
||||||
diesel::delete(posts.filter(id.eq(post_id)))
|
|
||||||
.execute(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't delete post."))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
errors::{RbError, RbResult},
|
|
||||||
schema::{sections, sections::dsl::*},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Queryable, Serialize)]
|
|
||||||
pub struct Section
|
|
||||||
{
|
|
||||||
pub id: Uuid,
|
|
||||||
pub title: String,
|
|
||||||
pub shortname: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub is_default: bool,
|
|
||||||
pub has_titles: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Insertable)]
|
|
||||||
#[table_name = "sections"]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct NewSection
|
|
||||||
{
|
|
||||||
title: String,
|
|
||||||
pub shortname: String,
|
|
||||||
description: Option<String>,
|
|
||||||
is_default: Option<bool>,
|
|
||||||
has_titles: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, AsChangeset)]
|
|
||||||
#[table_name = "sections"]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PatchSection
|
|
||||||
{
|
|
||||||
title: Option<String>,
|
|
||||||
shortname: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
is_default: Option<bool>,
|
|
||||||
has_titles: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Section>>
|
|
||||||
{
|
|
||||||
Ok(sections
|
|
||||||
.offset(offset_.into())
|
|
||||||
.limit(limit_.into())
|
|
||||||
.load(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't query sections."))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(conn: &PgConnection, new_post: &NewSection) -> RbResult<Section>
|
|
||||||
{
|
|
||||||
Ok(insert_into(sections)
|
|
||||||
.values(new_post)
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't insert section."))?)
|
|
||||||
|
|
||||||
// TODO check for conflict?
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchSection) -> RbResult<Section>
|
|
||||||
{
|
|
||||||
Ok(diesel::update(sections.filter(id.eq(post_id)))
|
|
||||||
.set(patch_post)
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't update section."))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
|
||||||
{
|
|
||||||
diesel::delete(sections.filter(id.eq(post_id)))
|
|
||||||
.execute(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't delete section."))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,7 +1,4 @@
|
||||||
//! Handles refresh token-related database operations.
|
|
||||||
|
|
||||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -9,8 +6,7 @@ use crate::{
|
||||||
schema::{refresh_tokens, refresh_tokens::dsl::*},
|
schema::{refresh_tokens, refresh_tokens::dsl::*},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A refresh token as stored in the database
|
#[derive(Queryable)]
|
||||||
#[derive(Queryable, Serialize)]
|
|
||||||
pub struct RefreshToken
|
pub struct RefreshToken
|
||||||
{
|
{
|
||||||
pub token: Vec<u8>,
|
pub token: Vec<u8>,
|
||||||
|
@ -19,8 +15,7 @@ pub struct RefreshToken
|
||||||
pub last_used_at: Option<chrono::NaiveDateTime>,
|
pub last_used_at: Option<chrono::NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A new refresh token to be added into the database
|
#[derive(Insertable)]
|
||||||
#[derive(Deserialize, Insertable)]
|
|
||||||
#[table_name = "refresh_tokens"]
|
#[table_name = "refresh_tokens"]
|
||||||
pub struct NewRefreshToken
|
pub struct NewRefreshToken
|
||||||
{
|
{
|
||||||
|
@ -29,84 +24,39 @@ pub struct NewRefreshToken
|
||||||
pub expires_at: chrono::NaiveDateTime,
|
pub expires_at: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, AsChangeset)]
|
pub fn all(conn: &PgConnection) -> RbResult<Vec<RefreshToken>>
|
||||||
#[table_name = "refresh_tokens"]
|
|
||||||
pub struct PatchRefreshToken
|
|
||||||
{
|
{
|
||||||
pub expires_at: Option<chrono::NaiveDateTime>,
|
refresh_tokens
|
||||||
pub last_used_at: Option<chrono::NaiveDateTime>,
|
.load::<RefreshToken>(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't get all refresh tokens."))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<RefreshToken>>
|
pub fn create(conn: &PgConnection, new_refresh_token: &NewRefreshToken) -> RbResult<()>
|
||||||
{
|
{
|
||||||
Ok(refresh_tokens
|
insert_into(refresh_tokens)
|
||||||
.offset(offset_.into())
|
.values(new_refresh_token)
|
||||||
.limit(limit_.into())
|
.execute(conn)
|
||||||
.load(conn)
|
.map_err(|_| RbError::Custom("Couldn't insert refresh token."))?;
|
||||||
.map_err(|_| RbError::DbError("Couldn't query tokens."))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(conn: &PgConnection, new_token: &NewRefreshToken) -> RbResult<RefreshToken>
|
|
||||||
{
|
|
||||||
Ok(insert_into(refresh_tokens)
|
|
||||||
.values(new_token)
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't insert refresh token."))?)
|
|
||||||
|
|
||||||
// TODO check for conflict?
|
// TODO check for conflict?
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(
|
|
||||||
conn: &PgConnection,
|
|
||||||
token_: &[u8],
|
|
||||||
patch_token: &PatchRefreshToken,
|
|
||||||
) -> RbResult<RefreshToken>
|
|
||||||
{
|
|
||||||
Ok(diesel::update(refresh_tokens.filter(token.eq(token_)))
|
|
||||||
.set(patch_token)
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't update token."))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(conn: &PgConnection, token_: &[u8]) -> RbResult<()>
|
|
||||||
{
|
|
||||||
diesel::delete(refresh_tokens.filter(token.eq(token_)))
|
|
||||||
.execute(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't delete token."))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the token & user data associated with the given refresh token value.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `conn` - database connection to use
|
|
||||||
/// * `token_val` - token value to search for
|
|
||||||
pub fn find_with_user(
|
pub fn find_with_user(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
token_: &[u8],
|
token_val: &[u8],
|
||||||
) -> Option<(RefreshToken, super::users::User)>
|
) -> Option<(RefreshToken, super::users::User)>
|
||||||
{
|
{
|
||||||
// TODO actually check for errors here
|
// TODO actually check for errors here
|
||||||
refresh_tokens
|
refresh_tokens
|
||||||
.inner_join(crate::schema::users::dsl::users)
|
.inner_join(crate::schema::users::dsl::users)
|
||||||
.filter(token.eq(token_))
|
.filter(token.eq(token_val))
|
||||||
.first::<(RefreshToken, super::users::User)>(conn)
|
.first::<(RefreshToken, super::users::User)>(conn)
|
||||||
.map_err(|_| RbError::DbError("Couldn't get refresh token & user."))
|
.map_err(|_| RbError::Custom("Couldn't get refresh token & user."))
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates a token's `last_used_at` column value.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `conn` - database connection to use
|
|
||||||
/// * `token_` - value of the refresh token to update
|
|
||||||
/// * `last_used_at_` - date value to update column with
|
|
||||||
///
|
|
||||||
/// **NOTE**: argument names use trailing underscores as to not conflict with Diesel's imported dsl
|
|
||||||
/// names.
|
|
||||||
pub fn update_last_used_at(
|
pub fn update_last_used_at(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
token_: &[u8],
|
token_: &[u8],
|
||||||
|
|
|
@ -18,7 +18,7 @@ pub struct User
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, Deserialize)]
|
#[derive(Insertable, AsChangeset, Deserialize)]
|
||||||
#[table_name = "users"]
|
#[table_name = "users"]
|
||||||
pub struct NewUser
|
pub struct NewUser
|
||||||
{
|
{
|
||||||
|
@ -27,22 +27,11 @@ pub struct NewUser
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, AsChangeset)]
|
pub fn all(conn: &PgConnection) -> RbResult<Vec<User>>
|
||||||
#[table_name = "users"]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PatchSection
|
|
||||||
{
|
{
|
||||||
username: Option<String>,
|
users
|
||||||
admin: Option<bool>,
|
.load::<User>(conn)
|
||||||
}
|
.map_err(|_| RbError::DbError("Couldn't get all users."))
|
||||||
|
|
||||||
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<User>>
|
|
||||||
{
|
|
||||||
Ok(users
|
|
||||||
.offset(offset_.into())
|
|
||||||
.limit(limit_.into())
|
|
||||||
.load(conn)
|
|
||||||
.map_err(|_| RbError::DbError("Couldn't query users."))?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find(conn: &PgConnection, user_id: Uuid) -> Option<User>
|
pub fn find(conn: &PgConnection, user_id: Uuid) -> Option<User>
|
||||||
|
@ -58,12 +47,6 @@ pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult<User>
|
||||||
.map_err(|_| RbError::DbError("Couldn't find users by username."))?)
|
.map_err(|_| RbError::DbError("Couldn't find users by username."))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a new user into the database
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `conn` - database connection to use
|
|
||||||
/// * `new_user` - user to insert
|
|
||||||
pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||||
{
|
{
|
||||||
let count = diesel::insert_into(users)
|
let count = diesel::insert_into(users)
|
||||||
|
@ -78,31 +61,19 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Either create a new user or update an existing one on conflict.
|
pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||||
///
|
{
|
||||||
/// # Arguments
|
diesel::insert_into(users)
|
||||||
///
|
.values(new_user)
|
||||||
/// * `conn` - database connection to use
|
.on_conflict(username)
|
||||||
/// * `new_user` - user to insert/update
|
.do_update()
|
||||||
// pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
.set(new_user)
|
||||||
// {
|
.execute(conn)
|
||||||
// diesel::insert_into(users)
|
.map_err(|_| RbError::DbError("Couldn't create or update user."))?;
|
||||||
// .values(new_user)
|
|
||||||
// .on_conflict(username)
|
|
||||||
// .do_update()
|
|
||||||
// .set(new_user)
|
|
||||||
// .execute(conn)
|
|
||||||
// .map_err(|_| RbError::DbError("Couldn't create or update user."))?;
|
|
||||||
|
|
||||||
// Ok(())
|
Ok(())
|
||||||
// }
|
}
|
||||||
|
|
||||||
/// Delete the user with the given ID.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// `conn` - database connection to use
|
|
||||||
/// `user_id` - ID of user to delete
|
|
||||||
pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
||||||
{
|
{
|
||||||
diesel::delete(users.filter(id.eq(user_id)))
|
diesel::delete(users.filter(id.eq(user_id)))
|
||||||
|
@ -112,14 +83,6 @@ pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block a user given an ID.
|
|
||||||
/// In practice, this means updating the user's entry so that the `blocked` column is set to
|
|
||||||
/// `true`.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// `conn` - database connection to use
|
|
||||||
/// `user_id` - ID of user to block
|
|
||||||
pub fn block(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
pub fn block(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
||||||
{
|
{
|
||||||
diesel::update(users.filter(id.eq(user_id)))
|
diesel::update(users.filter(id.eq(user_id)))
|
||||||
|
|
|
@ -61,7 +61,7 @@ impl RbError
|
||||||
RbError::AuthInvalidRefreshToken => "This refresh token is not valid.",
|
RbError::AuthInvalidRefreshToken => "This refresh token is not valid.",
|
||||||
RbError::AuthDuplicateRefreshToken => {
|
RbError::AuthDuplicateRefreshToken => {
|
||||||
"This refresh token has already been used. The user has been blocked."
|
"This refresh token has already been used. The user has been blocked."
|
||||||
},
|
}
|
||||||
RbError::AuthMissingHeader => "Missing Authorization header.",
|
RbError::AuthMissingHeader => "Missing Authorization header.",
|
||||||
|
|
||||||
RbError::UMDuplicateUser => "This user already exists.",
|
RbError::UMDuplicateUser => "This user already exists.",
|
||||||
|
@ -87,8 +87,4 @@ impl<'r> Responder<'r, 'static> for RbError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type alias for results that can return an RbError
|
|
||||||
pub type RbResult<T> = std::result::Result<T, RbError>;
|
pub type RbResult<T> = std::result::Result<T, RbError>;
|
||||||
|
|
||||||
/// Type alias for optional results that can fail & return an RbError
|
|
||||||
pub type RbOption<T> = RbResult<Option<T>>;
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ use sha2::Sha256;
|
||||||
|
|
||||||
use crate::{auth::jwt::Claims, errors::RbError, RbConfig};
|
use crate::{auth::jwt::Claims, errors::RbError, RbConfig};
|
||||||
|
|
||||||
/// Extracts an "Authorization: Bearer" string from the headers.
|
/// Extracts a "Authorization: Bearer" string from the headers.
|
||||||
pub struct Bearer<'a>(&'a str);
|
pub struct Bearer<'a>(&'a str);
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
|
@ -22,18 +22,21 @@ impl<'r> FromRequest<'r> for Bearer<'r>
|
||||||
{
|
{
|
||||||
// If the header isn't present, just forward to the next route
|
// If the header isn't present, just forward to the next route
|
||||||
let header = match req.headers().get_one("Authorization") {
|
let header = match req.headers().get_one("Authorization") {
|
||||||
None => return Outcome::Forward(()),
|
None => return Outcome::Failure((Status::BadRequest, Self::Error::AuthMissingHeader)),
|
||||||
Some(val) => val,
|
Some(val) => val,
|
||||||
};
|
};
|
||||||
|
|
||||||
if header.starts_with("Bearer ") {
|
if !header.starts_with("Bearer ") {
|
||||||
match header.get(7..) {
|
return Outcome::Forward(());
|
||||||
Some(s) => Outcome::Success(Self(s)),
|
|
||||||
None => Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Outcome::Forward(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the jwt token from the header
|
||||||
|
let auth_string = match header.get(7..) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
|
||||||
|
};
|
||||||
|
|
||||||
|
Outcome::Success(Self(auth_string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,16 +63,17 @@ impl<'r> FromRequest<'r> for Jwt
|
||||||
Status::InternalServerError,
|
Status::InternalServerError,
|
||||||
Self::Error::Custom("Failed to do Hmac thing."),
|
Self::Error::Custom("Failed to do Hmac thing."),
|
||||||
))
|
))
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify token using key
|
// Verify token using key
|
||||||
match bearer.verify_with_key(&key) {
|
let claims: Claims = match bearer.verify_with_key(&key) {
|
||||||
Ok(claims) => Outcome::Success(Self(claims)),
|
Ok(claims) => claims,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized))
|
return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized))
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Outcome::Success(Self(claims))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,10 +91,10 @@ impl<'r> FromRequest<'r> for User
|
||||||
|
|
||||||
// Verify key hasn't yet expired
|
// Verify key hasn't yet expired
|
||||||
if chrono::Utc::now().timestamp() > claims.exp {
|
if chrono::Utc::now().timestamp() > claims.exp {
|
||||||
Outcome::Failure((Status::Forbidden, Self::Error::AuthTokenExpired))
|
return Outcome::Failure((Status::Forbidden, Self::Error::AuthTokenExpired));
|
||||||
} else {
|
|
||||||
Outcome::Success(Self(claims))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Outcome::Success(Self(claims))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -12,13 +12,11 @@ use figment::{
|
||||||
providers::{Env, Format, Yaml},
|
providers::{Env, Format, Yaml},
|
||||||
Figment,
|
Figment,
|
||||||
};
|
};
|
||||||
#[cfg(any(feature = "web", feature = "docs"))]
|
|
||||||
use rocket::fs;
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
fairing::AdHoc,
|
fairing::AdHoc,
|
||||||
http::Status,
|
http::Status,
|
||||||
serde::json::{json, Value},
|
serde::json::{json, Value},
|
||||||
Build, Orbit, Request, Rocket,
|
Build, Request, Rocket, Orbit,
|
||||||
};
|
};
|
||||||
use rocket_sync_db_pools::database;
|
use rocket_sync_db_pools::database;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -28,9 +26,7 @@ pub mod auth;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod guards;
|
pub mod guards;
|
||||||
pub mod posts;
|
|
||||||
pub(crate) mod schema;
|
pub(crate) mod schema;
|
||||||
pub mod sections;
|
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
|
@ -68,7 +64,8 @@ async fn create_admin_user<'a>(rocket: &'a Rocket<Orbit>)
|
||||||
.await
|
.await
|
||||||
.expect("database connection");
|
.expect("database connection");
|
||||||
conn.run(move |c| {
|
conn.run(move |c| {
|
||||||
admin::create_admin_user(c, &admin_user, &admin_pass).expect("failed to create admin user")
|
admin::create_admin_user(c, &admin_user, &admin_pass)
|
||||||
|
.expect("failed to create admin user")
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -96,10 +93,7 @@ fn rocket() -> _
|
||||||
.merge(Yaml::file("Rb.yaml").nested())
|
.merge(Yaml::file("Rb.yaml").nested())
|
||||||
.merge(Env::prefixed("RB_").global());
|
.merge(Env::prefixed("RB_").global());
|
||||||
|
|
||||||
// This mut is necessary when the "docs" or "web" feature is enabled, as these further modify
|
rocket::custom(figment)
|
||||||
// the instance variable
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut instance = rocket::custom(figment)
|
|
||||||
.attach(RbDbConn::fairing())
|
.attach(RbDbConn::fairing())
|
||||||
.attach(AdHoc::try_on_ignite(
|
.attach(AdHoc::try_on_ignite(
|
||||||
"Run database migrations",
|
"Run database migrations",
|
||||||
|
@ -114,33 +108,6 @@ fn rocket() -> _
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/api/admin",
|
"/api/admin",
|
||||||
routes![admin::create_user, admin::get_user_info],
|
routes![admin::get_users, admin::create_user, admin::get_user_info],
|
||||||
)
|
)
|
||||||
.mount("/api/sections", routes![sections::create_section])
|
|
||||||
.mount("/api/posts", routes![posts::get, posts::create]);
|
|
||||||
|
|
||||||
// It's weird that this is allowed, but the line on its own isn't
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
{
|
|
||||||
instance = instance.mount(
|
|
||||||
"/",
|
|
||||||
fs::FileServer::new(
|
|
||||||
"/var/www/html/web",
|
|
||||||
fs::Options::Index | fs::Options::NormalizeDirs,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "docs")]
|
|
||||||
{
|
|
||||||
instance = instance.mount(
|
|
||||||
"/docs",
|
|
||||||
fs::FileServer::new(
|
|
||||||
"/var/www/html/docs",
|
|
||||||
fs::Options::Index | fs::Options::NormalizeDirs,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance
|
|
||||||
}
|
}
|
||||||
|
|
58
src/posts.rs
58
src/posts.rs
|
@ -1,58 +0,0 @@
|
||||||
use rocket::serde::json::Json;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
db,
|
|
||||||
errors::{RbOption, RbResult},
|
|
||||||
guards::Admin,
|
|
||||||
RbDbConn,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[get("/?<offset>&<limit>")]
|
|
||||||
pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<db::Post>>>
|
|
||||||
{
|
|
||||||
Ok(Json(
|
|
||||||
conn.run(move |c| db::posts::get(c, offset, limit)).await?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/", data = "<new_post>")]
|
|
||||||
pub async fn create(
|
|
||||||
_admin: Admin,
|
|
||||||
conn: RbDbConn,
|
|
||||||
new_post: Json<db::NewPost>,
|
|
||||||
) -> RbResult<Json<db::Post>>
|
|
||||||
{
|
|
||||||
Ok(Json(
|
|
||||||
conn.run(move |c| db::posts::create(c, &new_post.into_inner()))
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/<id>")]
|
|
||||||
pub async fn find(conn: RbDbConn, id: uuid::Uuid) -> RbOption<Json<db::Post>>
|
|
||||||
{
|
|
||||||
Ok(conn
|
|
||||||
.run(move |c| db::posts::find(c, &id))
|
|
||||||
.await?
|
|
||||||
.and_then(|p| Some(Json(p))))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[patch("/<id>", data = "<patch_post>")]
|
|
||||||
pub async fn patch(
|
|
||||||
_admin: Admin,
|
|
||||||
conn: RbDbConn,
|
|
||||||
id: uuid::Uuid,
|
|
||||||
patch_post: Json<db::PatchPost>,
|
|
||||||
) -> RbResult<Json<db::Post>>
|
|
||||||
{
|
|
||||||
Ok(Json(
|
|
||||||
conn.run(move |c| db::posts::update(c, &id, &patch_post.into_inner()))
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[delete("/<id>")]
|
|
||||||
pub async fn delete(_admin: Admin, conn: RbDbConn, id: uuid::Uuid) -> RbResult<()>
|
|
||||||
{
|
|
||||||
Ok(conn.run(move |c| db::posts::delete(c, &id)).await?)
|
|
||||||
}
|
|
|
@ -1,13 +1,3 @@
|
||||||
table! {
|
|
||||||
posts (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
section_id -> Uuid,
|
|
||||||
title -> Nullable<Varchar>,
|
|
||||||
publish_date -> Date,
|
|
||||||
content -> Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
refresh_tokens (token) {
|
refresh_tokens (token) {
|
||||||
token -> Bytea,
|
token -> Bytea,
|
||||||
|
@ -17,17 +7,6 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
|
||||||
sections (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
title -> Varchar,
|
|
||||||
shortname -> Varchar,
|
|
||||||
description -> Nullable<Text>,
|
|
||||||
is_default -> Bool,
|
|
||||||
has_titles -> Bool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (id) {
|
users (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
@ -38,7 +17,6 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinable!(posts -> sections (section_id));
|
|
||||||
joinable!(refresh_tokens -> users (user_id));
|
joinable!(refresh_tokens -> users (user_id));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(posts, refresh_tokens, sections, users,);
|
allow_tables_to_appear_in_same_query!(refresh_tokens, users,);
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
//! This module handles management of site sections (aka blogs).
|
|
||||||
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
|
|
||||||
use crate::{db, errors::RbResult, guards::Admin, RbDbConn};
|
|
||||||
|
|
||||||
/// Route for creating a new section.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `_admin` - guard ensuring user is admin
|
|
||||||
/// * `conn` - guard providing a connection to the database
|
|
||||||
/// * `new_section` - Json-encoded NewSection object
|
|
||||||
#[post("/", data = "<new_section>")]
|
|
||||||
pub async fn create_section(
|
|
||||||
_admin: Admin,
|
|
||||||
conn: RbDbConn,
|
|
||||||
new_section: Json<db::NewSection>,
|
|
||||||
) -> RbResult<Json<db::Section>>
|
|
||||||
{
|
|
||||||
Ok(Json(
|
|
||||||
conn.run(move |c| db::sections::create(c, &new_section.into_inner()))
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ import requests
|
||||||
|
|
||||||
|
|
||||||
class RbClient:
|
class RbClient:
|
||||||
def __init__(self, username = "admin", password = "password", base_url = "http://localhost:8000/api"):
|
def __init__(self, username, password, base_url = "http://localhost:8000/api"):
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
@ -17,7 +17,6 @@ class RbClient:
|
||||||
})
|
})
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
print(r.text)
|
|
||||||
raise Exception("Couldn't login")
|
raise Exception("Couldn't login")
|
||||||
|
|
||||||
res = r.json()
|
res = r.json()
|
||||||
|
@ -57,15 +56,9 @@ class RbClient:
|
||||||
def get(self, url, *args, **kwargs):
|
def get(self, url, *args, **kwargs):
|
||||||
return self._request("GET", f"{self.base_url}{url}", *args, **kwargs)
|
return self._request("GET", f"{self.base_url}{url}", *args, **kwargs)
|
||||||
|
|
||||||
def post(self, url, *args, **kwargs):
|
|
||||||
return self._request("POST", f"{self.base_url}{url}", *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
client = RbClient()
|
client = RbClient("admin", "password")
|
||||||
|
|
||||||
# print(client.get("/admin/users").json())
|
print(client.get("/admin/users").json())
|
||||||
client.post("/sections", json={
|
|
||||||
"title": "this is a title"
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
node_modules
|
# build output
|
||||||
.DS_Store
|
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
|
||||||
*.local
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
.snowpack/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
## force pnpm to hoist
|
||||||
|
shamefully-hoist = true
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": ["johnsoncodehk.volar"]
|
|
||||||
}
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Welcome to [Astro](https://astro.build)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
│ ├── robots.txt
|
||||||
|
│ └── favicon.ico
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── Tour.astro
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
|:----------------|:--------------------------------------------|
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm start` | Starts local dev server at `localhost:3000` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://astro.build/chat).
|
|
@ -0,0 +1,18 @@
|
||||||
|
export default {
|
||||||
|
// projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project.
|
||||||
|
// pages: './src/pages', // Path to Astro components, pages, and data
|
||||||
|
// dist: './dist', // When running `astro build`, path to final static output
|
||||||
|
// public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing.
|
||||||
|
buildOptions: {
|
||||||
|
// site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
|
||||||
|
sitemap: true, // Generate sitemap (set to "false" to disable)
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
// hostname: 'localhost', // The hostname to run the dev server on.
|
||||||
|
// port: 3000, // The port to run the dev server on.
|
||||||
|
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
|
||||||
|
},
|
||||||
|
renderers: [
|
||||||
|
"@astrojs/renderer-svelte"
|
||||||
|
],
|
||||||
|
};
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vite App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,20 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "rusty-bever",
|
"name": "@example/starter",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"start": "astro dev",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "astro build"
|
||||||
"serve": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"vue": "^3.2.16"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^16.10.3",
|
"astro": "0.19.0-next.2",
|
||||||
"@vitejs/plugin-vue": "^1.9.3",
|
"@astrojs/renderer-svelte": "^0.1.1"
|
||||||
"miragejs": "^0.1.42",
|
|
||||||
"typescript": "^4.4.3",
|
|
||||||
"vite": "^2.6.4",
|
|
||||||
"vue-tsc": "^0.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<svg width="193" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
#flame { fill: #FF5D01; }
|
||||||
|
#a { fill: #000014; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#a { fill: #fff; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M131.496 18.929c1.943 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.746 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.224 180.224 0 00-52.01 17.557l43.52-142.281c1.989-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.085 1.157a16 16 0 016.488 4.806z" fill="url(#paint0_linear)"/>
|
||||||
|
<path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M136.678 180.151c-7.14 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.962 10.367-1.962 13.902 0 0-1.055 17.355 11.016 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.973-19.87 5.977-3.79 12.616-8.001 17.192-16.449a31.013 31.013 0 003.744-14.82c0-3.299-.513-6.479-1.463-9.463z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
<svg width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
#flame { fill: #FF5D01; }
|
||||||
|
#a { fill: #000014; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#a { fill: #fff; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" />
|
||||||
|
<path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
|
@ -0,0 +1,28 @@
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||||
|
font-size: 1rem;
|
||||||
|
--user-font-scale: 1rem - 16px;
|
||||||
|
font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: #111827;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
:root {
|
||||||
|
--font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono',
|
||||||
|
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
|
||||||
|
--color-light: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-light: #1f2937;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > div {
|
||||||
|
font-size: clamp(2rem, -0.4742rem + 6.1856vw, 2.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
header > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
header img {
|
||||||
|
width: 2em;
|
||||||
|
height: 2.667em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: clamp(1.5rem, 1rem + 1.25vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
gap: 1em;
|
||||||
|
font-size: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter > pre {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 3ch;
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
// This starter template is using Vue 3 <script setup> SFCs
|
|
||||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<img alt="Vue logo" src="./assets/logo.png" />
|
|
||||||
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#app {
|
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-top: 60px;
|
|
||||||
}
|
|
||||||
</style>
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,68 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps<{ msg: string }>()
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
let test = ref("yeet")
|
|
||||||
|
|
||||||
fetch("/api/users").then(
|
|
||||||
res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
console.log("ah chucks")
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
).then(
|
|
||||||
json => test.value = json
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
|
|
||||||
<p>{{ test }}</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Recommended IDE setup:
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
|
|
||||||
+
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>See <code>README.md</code> for more information.</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank">
|
|
||||||
Vite Docs
|
|
||||||
</a>
|
|
||||||
|
|
|
||||||
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button type="button" @click="count++">count is: {{ count }}</button>
|
|
||||||
<p>
|
|
||||||
Edit
|
|
||||||
<code>components/HelloWorld.vue</code> to test hot module replacement.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin: 0 0.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background-color: #eee;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #304455;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subtract() {
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="svelte" class="counter">
|
||||||
|
<button on:click={subtract}>-</button>
|
||||||
|
<pre>{ count }</pre>
|
||||||
|
<button on:click={add}>+</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
import { Markdown } from 'astro/components';
|
||||||
|
---
|
||||||
|
<article>
|
||||||
|
<div class="banner">
|
||||||
|
<p><strong>🧑🚀 Seasoned astronaut?</strong> Delete this file. Have fun!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Markdown>
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
│ ├── robots.txt
|
||||||
|
│ └── favicon.ico
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── Tour.astro
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory.
|
||||||
|
Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
</Markdown>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>👀 Want to learn more?</h2>
|
||||||
|
<p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://astro.build/chat">Discord server</a>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
article {
|
||||||
|
padding-top: 2em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
margin-top: 2em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
max-width: 70ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
background: var(--color-light);
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
padding-left: 0.75em;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
background: var(--color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
code:not(.tree) {
|
||||||
|
padding: 0.125em;
|
||||||
|
margin: 0 -0.125em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,8 +0,0 @@
|
||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
declare module '*.vue' {
|
|
||||||
import { DefineComponent } from 'vue'
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
|
||||||
const component: DefineComponent<{}, {}, any>
|
|
||||||
export default component
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>huh</h1>
|
||||||
|
<p>lol</p>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,10 +0,0 @@
|
||||||
import { createApp } from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
// @ts-ignore
|
|
||||||
import { makeServer } from "./server"
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
makeServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<ul id="nav-bar">
|
||||||
|
<li class="nav-bar-item"><a href="/home">Home</a></li>
|
||||||
|
<li class="nav-bar-item"><a href="/blog">Blog</a></li>
|
||||||
|
<li class="nav-bar-item"><a href="/microblog">Microblog</a></li>
|
||||||
|
<li class="nav-bar-item"><a href="/devlogs">Devlogs</a></li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul#nav-bar {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 200px;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#nav-bar li {
|
||||||
|
text-align: center;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.nav-bar-item a {
|
||||||
|
display: block;
|
||||||
|
color: #000;
|
||||||
|
padding: 8px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.nav-bar-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.nav-bar-item a:hover {
|
||||||
|
background-color: #555;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,27 +0,0 @@
|
||||||
// src/server.js
|
|
||||||
import { createServer, Model } from "miragejs"
|
|
||||||
|
|
||||||
export function makeServer({ environment = "development" } = {}) {
|
|
||||||
let server = createServer({
|
|
||||||
environment,
|
|
||||||
|
|
||||||
models: {
|
|
||||||
user: Model,
|
|
||||||
},
|
|
||||||
|
|
||||||
seeds(server) {
|
|
||||||
server.create("user", { name: "Bob" })
|
|
||||||
server.create("user", { name: "Alice" })
|
|
||||||
},
|
|
||||||
|
|
||||||
routes() {
|
|
||||||
this.namespace = "api"
|
|
||||||
|
|
||||||
this.get("/users", (schema) => {
|
|
||||||
return schema.users.all()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
|
|
@ -1,15 +1,3 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"moduleResolution": "node"
|
||||||
"target": "esnext",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"sourceMap": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"lib": ["esnext", "dom"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()]
|
|
||||||
})
|
|
5511
web/yarn.lock
5511
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue