forked from Chewing_Bever/rusty-bever
Merge branch 'sections-backend' into develop
commit
1441e3e601
12
Rb.yaml
12
Rb.yaml
|
@ -23,13 +23,21 @@ debug:
|
|||
url: "postgres://rb:rb@localhost:5432/rb"
|
||||
|
||||
release:
|
||||
keep_alive: 5
|
||||
read_timeout: 5
|
||||
write_timeout: 5
|
||||
log_level: "normal"
|
||||
limits:
|
||||
forms: 32768
|
||||
|
||||
admin_user: "admin"
|
||||
admin_pass: "password"
|
||||
jwt:
|
||||
key: "secret"
|
||||
refresh_token_size: 64
|
||||
refresh_token_expire: 86400
|
||||
# Just 5 seconds for debugging
|
||||
refresh_token_expire: 60
|
||||
|
||||
databases:
|
||||
postgres_rb:
|
||||
url: "postgres://rb:rb@db:5432/rb"
|
||||
url: "postgres://rb:rb@localhost:5432/rb"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- 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;
|
|
@ -0,0 +1,56 @@
|
|||
-- 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,
|
||||
-- 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
|
||||
match_arm_blocks = true
|
||||
match_arm_leading_pipes = "Never"
|
||||
match_block_trailing_comma = false
|
||||
match_block_trailing_comma = true
|
||||
max_width = 100
|
||||
merge_derives = true
|
||||
newline_style = "Auto"
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
//! 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 users;
|
||||
|
||||
pub use sections::{NewSection, Section};
|
||||
pub use tokens::{NewRefreshToken, RefreshToken};
|
||||
pub use users::{NewUser, User};
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
use chrono::NaiveDate;
|
||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::{RbError, RbResult},
|
||||
schema::{posts, posts::dsl::*},
|
||||
};
|
||||
|
||||
#[derive(Queryable)]
|
||||
pub struct Post
|
||||
{
|
||||
pub id: Uuid,
|
||||
pub section_id: Uuid,
|
||||
pub title: Option<String>,
|
||||
pub publish_date: NaiveDate,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "posts"]
|
||||
pub struct NewPost
|
||||
{
|
||||
pub section_id: Uuid,
|
||||
pub title: Option<String>,
|
||||
pub publish_date: NaiveDate,
|
||||
}
|
||||
|
||||
/// Returns all posts in the database; should be used with care as this method could quickly return
|
||||
/// a large amount of data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - a reference to a database connection
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<Post>>
|
||||
{
|
||||
posts
|
||||
.load::<Post>(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't get all posts."))
|
||||
}
|
||||
|
||||
/// Insert a new post into the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - reference to a database connection
|
||||
/// * `new_post` - the new post object to insert
|
||||
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<()>
|
||||
{
|
||||
insert_into(posts)
|
||||
.values(new_post)
|
||||
.execute(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't insert post."))?;
|
||||
|
||||
// TODO check for conflict?
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
//! Handles all section-related database operations.
|
||||
|
||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::{RbError, RbResult},
|
||||
schema::{sections, sections::dsl::*},
|
||||
};
|
||||
|
||||
/// Represents a section contained in the database.
|
||||
#[derive(Queryable)]
|
||||
pub struct Section
|
||||
{
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub is_default: bool,
|
||||
pub has_titles: bool,
|
||||
}
|
||||
|
||||
/// A new section to be added into the database.
|
||||
#[derive(Deserialize, Insertable)]
|
||||
#[table_name = "sections"]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NewSection
|
||||
{
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
is_default: Option<bool>,
|
||||
has_titles: Option<bool>,
|
||||
}
|
||||
|
||||
/// Returns all sections in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - reference to a database connection
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<Section>>
|
||||
{
|
||||
sections
|
||||
.load::<Section>(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't get all sections"))
|
||||
}
|
||||
|
||||
/// Inserts a new section into the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - reference to a database connection
|
||||
/// * `new_section` - the new section to be added
|
||||
pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<()>
|
||||
{
|
||||
insert_into(sections)
|
||||
.values(new_section)
|
||||
.execute(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't insert section."))?;
|
||||
|
||||
// TODO check for conflict?
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
//! Handles refresh token-related database operations.
|
||||
|
||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -6,6 +8,7 @@ use crate::{
|
|||
schema::{refresh_tokens, refresh_tokens::dsl::*},
|
||||
};
|
||||
|
||||
/// A refresh token as stored in the database
|
||||
#[derive(Queryable)]
|
||||
pub struct RefreshToken
|
||||
{
|
||||
|
@ -15,6 +18,7 @@ pub struct RefreshToken
|
|||
pub last_used_at: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
/// A new refresh token to be added into the database
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "refresh_tokens"]
|
||||
pub struct NewRefreshToken
|
||||
|
@ -24,6 +28,12 @@ pub struct NewRefreshToken
|
|||
pub expires_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
// TODO add pagination as this could grow very quickly
|
||||
/// Returns all refresh tokens contained in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<RefreshToken>>
|
||||
{
|
||||
refresh_tokens
|
||||
|
@ -31,18 +41,30 @@ pub fn all(conn: &PgConnection) -> RbResult<Vec<RefreshToken>>
|
|||
.map_err(|_| RbError::DbError("Couldn't get all refresh tokens."))
|
||||
}
|
||||
|
||||
/// Insert a new refresh token into the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `new_refresh_token` - token to insert
|
||||
pub fn create(conn: &PgConnection, new_refresh_token: &NewRefreshToken) -> RbResult<()>
|
||||
{
|
||||
insert_into(refresh_tokens)
|
||||
.values(new_refresh_token)
|
||||
.execute(conn)
|
||||
.map_err(|_| RbError::Custom("Couldn't insert refresh token."))?;
|
||||
.map_err(|_| RbError::DbError("Couldn't insert refresh token."))?;
|
||||
|
||||
// TODO check for conflict?
|
||||
|
||||
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(
|
||||
conn: &PgConnection,
|
||||
token_val: &[u8],
|
||||
|
@ -53,10 +75,20 @@ pub fn find_with_user(
|
|||
.inner_join(crate::schema::users::dsl::users)
|
||||
.filter(token.eq(token_val))
|
||||
.first::<(RefreshToken, super::users::User)>(conn)
|
||||
.map_err(|_| RbError::Custom("Couldn't get refresh token & user."))
|
||||
.map_err(|_| RbError::DbError("Couldn't get refresh token & user."))
|
||||
.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(
|
||||
conn: &PgConnection,
|
||||
token_: &[u8],
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Handles user-related database operations.
|
||||
|
||||
use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
@ -7,6 +9,7 @@ use crate::{
|
|||
schema::{users, users::dsl::*},
|
||||
};
|
||||
|
||||
/// A user as stored in the database.
|
||||
#[derive(Queryable, Serialize)]
|
||||
pub struct User
|
||||
{
|
||||
|
@ -18,6 +21,7 @@ pub struct User
|
|||
pub admin: bool,
|
||||
}
|
||||
|
||||
/// A new user to add to the database.
|
||||
#[derive(Insertable, AsChangeset, Deserialize)]
|
||||
#[table_name = "users"]
|
||||
pub struct NewUser
|
||||
|
@ -27,6 +31,11 @@ pub struct NewUser
|
|||
pub admin: bool,
|
||||
}
|
||||
|
||||
/// Returns all users in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<User>>
|
||||
{
|
||||
users
|
||||
|
@ -34,11 +43,23 @@ pub fn all(conn: &PgConnection) -> RbResult<Vec<User>>
|
|||
.map_err(|_| RbError::DbError("Couldn't get all users."))
|
||||
}
|
||||
|
||||
/// Find a user with a given ID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `user_id` - ID to search for
|
||||
pub fn find(conn: &PgConnection, user_id: Uuid) -> Option<User>
|
||||
{
|
||||
users.find(user_id).first::<User>(conn).ok()
|
||||
}
|
||||
|
||||
/// Find a user with a given username.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `username_` - username to search for
|
||||
pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult<User>
|
||||
{
|
||||
Ok(users
|
||||
|
@ -47,6 +68,12 @@ pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult<User>
|
|||
.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<()>
|
||||
{
|
||||
let count = diesel::insert_into(users)
|
||||
|
@ -61,6 +88,12 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Either create a new user or update an existing one on conflict.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `new_user` - user to insert/update
|
||||
pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||
{
|
||||
diesel::insert_into(users)
|
||||
|
@ -74,6 +107,12 @@ pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
|||
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<()>
|
||||
{
|
||||
diesel::delete(users.filter(id.eq(user_id)))
|
||||
|
@ -83,6 +122,14 @@ pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
|||
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<()>
|
||||
{
|
||||
diesel::update(users.filter(id.eq(user_id)))
|
||||
|
|
|
@ -61,7 +61,7 @@ impl RbError
|
|||
RbError::AuthInvalidRefreshToken => "This refresh token is not valid.",
|
||||
RbError::AuthDuplicateRefreshToken => {
|
||||
"This refresh token has already been used. The user has been blocked."
|
||||
}
|
||||
},
|
||||
RbError::AuthMissingHeader => "Missing Authorization header.",
|
||||
|
||||
RbError::UMDuplicateUser => "This user already exists.",
|
||||
|
|
|
@ -10,7 +10,7 @@ use sha2::Sha256;
|
|||
|
||||
use crate::{auth::jwt::Claims, errors::RbError, RbConfig};
|
||||
|
||||
/// Extracts a "Authorization: Bearer" string from the headers.
|
||||
/// Extracts an "Authorization: Bearer" string from the headers.
|
||||
pub struct Bearer<'a>(&'a str);
|
||||
|
||||
#[rocket::async_trait]
|
||||
|
@ -22,7 +22,7 @@ impl<'r> FromRequest<'r> for Bearer<'r>
|
|||
{
|
||||
// If the header isn't present, just forward to the next route
|
||||
let header = match req.headers().get_one("Authorization") {
|
||||
None => return Outcome::Failure((Status::BadRequest, Self::Error::AuthMissingHeader)),
|
||||
None => return Outcome::Forward(()),
|
||||
Some(val) => val,
|
||||
};
|
||||
|
||||
|
@ -31,12 +31,10 @@ impl<'r> FromRequest<'r> for Bearer<'r>
|
|||
}
|
||||
|
||||
// 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))
|
||||
match header.get(7..) {
|
||||
Some(s) => Outcome::Success(Self(s)),
|
||||
None => Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,14 +61,14 @@ impl<'r> FromRequest<'r> for Jwt
|
|||
Status::InternalServerError,
|
||||
Self::Error::Custom("Failed to do Hmac thing."),
|
||||
))
|
||||
}
|
||||
},
|
||||
};
|
||||
// Verify token using key
|
||||
let claims: Claims = match bearer.verify_with_key(&key) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => {
|
||||
return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Outcome::Success(Self(claims))
|
||||
|
|
|
@ -27,6 +27,7 @@ pub mod db;
|
|||
pub mod errors;
|
||||
pub mod guards;
|
||||
pub(crate) mod schema;
|
||||
pub mod sections;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
@ -110,4 +111,5 @@ fn rocket() -> _
|
|||
"/api/admin",
|
||||
routes![admin::get_users, admin::create_user, admin::get_user_info],
|
||||
)
|
||||
.mount("/api/sections", routes![sections::create_section])
|
||||
}
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
table! {
|
||||
posts (id) {
|
||||
id -> Uuid,
|
||||
section_id -> Uuid,
|
||||
title -> Nullable<Varchar>,
|
||||
publish_date -> Date,
|
||||
content -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
refresh_tokens (token) {
|
||||
token -> Bytea,
|
||||
|
@ -7,6 +17,16 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
sections (id) {
|
||||
id -> Uuid,
|
||||
title -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
is_default -> Bool,
|
||||
has_titles -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (id) {
|
||||
id -> Uuid,
|
||||
|
@ -17,6 +37,7 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
joinable!(posts -> sections (section_id));
|
||||
joinable!(refresh_tokens -> users (user_id));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(refresh_tokens, users,);
|
||||
allow_tables_to_appear_in_same_query!(posts, refresh_tokens, sections, users,);
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
//! 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<()>
|
||||
{
|
||||
Ok(conn
|
||||
.run(move |c| db::sections::create(c, &new_section.into_inner()))
|
||||
.await?)
|
||||
}
|
|
@ -2,7 +2,7 @@ import requests
|
|||
|
||||
|
||||
class RbClient:
|
||||
def __init__(self, username, password, base_url = "http://localhost:8000/api"):
|
||||
def __init__(self, username = "admin", password = "password", base_url = "http://localhost:8000/api"):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.base_url = base_url
|
||||
|
@ -17,6 +17,7 @@ class RbClient:
|
|||
})
|
||||
|
||||
if r.status_code != 200:
|
||||
print(r.text)
|
||||
raise Exception("Couldn't login")
|
||||
|
||||
res = r.json()
|
||||
|
@ -56,9 +57,15 @@ class RbClient:
|
|||
def get(self, 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__":
|
||||
client = RbClient("admin", "password")
|
||||
client = RbClient()
|
||||
|
||||
print(client.get("/admin/users").json())
|
||||
# print(client.get("/admin/users").json())
|
||||
client.post("/sections", json={
|
||||
"title": "this is a title"
|
||||
})
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
"build": "astro build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/renderer-svelte": "^0.1.1",
|
||||
"astro": "0.19.0-next.2",
|
||||
"@astrojs/renderer-svelte": "^0.1.1"
|
||||
"miragejs": "^0.1.41",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
|
||||
console.log("sup");
|
||||
let movies = [];
|
||||
|
||||
fetch("/api/movies").then(res => res.json()).then(res => {
|
||||
movies = res;
|
||||
console.log(movies);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each movies as { id, name, year }, i}
|
||||
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
|
||||
{i + 1}: {name} - {year}
|
||||
</a></li>
|
||||
{/each}
|
||||
</ul>
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
let count = 0;
|
||||
|
||||
function add() {
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
---
|
||||
---
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>huh</h1>
|
||||
<p>lol</p>
|
||||
<MirageTest />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,3 +1,24 @@
|
|||
---
|
||||
import { createServer } from "miragejs"
|
||||
import MirageTest from "../components/MirageTest"
|
||||
|
||||
createServer({
|
||||
routes() {
|
||||
this.namespace = "api"
|
||||
|
||||
this.get("/movies", () => {
|
||||
return {
|
||||
movies: [
|
||||
{ id: 1, name: "Inception", year: 2010 },
|
||||
{ id: 2, name: "Interstellar", year: 2014 },
|
||||
{ id: 3, name: "Dunkirk", year: 2017 },
|
||||
],
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
---
|
||||
|
||||
<html>
|
||||
<body>
|
||||
|
||||
|
@ -7,6 +28,8 @@
|
|||
<li class="nav-bar-item"><a href="/microblog">Microblog</a></li>
|
||||
<li class="nav-bar-item"><a href="/devlogs">Devlogs</a></li>
|
||||
</ul>
|
||||
|
||||
<MirageTest client:load />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
996
web/yarn.lock
996
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue