Compare commits
2 Commits
1b0e5d53c2
...
e868c87a7e
Author | SHA1 | Date | |
---|---|---|---|
e868c87a7e | |||
04dcf039c3 |
11
crates/frontend/src/api-client.ts
Normal file
11
crates/frontend/src/api-client.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import axios, { type AxiosInstance } from "axios";
|
||||||
|
|
||||||
|
const api_client: AxiosInstance = axios.create({
|
||||||
|
baseURL: import.meta.hot ? "http://localhost:54600/api/v1" : "/api/v1",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default api_client;
|
5
crates/frontend/src/components/Error.vue
Normal file
5
crates/frontend/src/components/Error.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<h1>
|
||||||
|
<slot></slot>
|
||||||
|
</h1>
|
||||||
|
</template>
|
@ -1,30 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import router from '@/router';
|
import router from "@/router";
|
||||||
|
import User from "@/services/user";
|
||||||
|
|
||||||
const email = defineModel("email");
|
const email = defineModel("email");
|
||||||
const password = defineModel("password");
|
const password = defineModel("password");
|
||||||
const errorMessage = ref(null);
|
const errorMessage = ref(null);
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
await fetch(import.meta.hot ? "http://localhost:54600/api/v1/user/login" : "/api/v1/user/login", {
|
await User.login(email.value, password.value)
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify({ email: email.value, password: password.value })
|
|
||||||
})
|
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
//const isJson = response.headers.get('content-type')?.includes('application/json');
|
if (response.status != 200) {
|
||||||
const data = await response.json();
|
return Promise.reject(response.data && response.data.message || response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = (data && data.message) || response.status;
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push({ path: '/me' });
|
const login = response.data.user.login;
|
||||||
|
router.push({ path: `/${login}` });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
errorMessage.value = error;
|
errorMessage.value = error;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: () => import('./views/Home.vue') },
|
{ path: "/", component: () => import("@/views/Home.vue") },
|
||||||
{ path: '/user/login', component: () => import('./views/SignIn.vue') },
|
{ path: "/user/login", component: () => import("@/views/SignIn.vue") },
|
||||||
{ path: '/me', component: () => import('./views/Me.vue') }
|
{ path: "/:user", name: "User", component: () => import("@/views/User.vue") },
|
||||||
|
{ path: "/:pathMatch(.*)*", component: () => import("@/views/Error.vue") }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
17
crates/frontend/src/services/user.ts
Normal file
17
crates/frontend/src/services/user.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import api_client from "@/api-client";
|
||||||
|
|
||||||
|
class User {
|
||||||
|
async login(email: string, password: string): Promise<JSON> {
|
||||||
|
return await api_client.post("/user/login", JSON.stringify({ email: email, password: password }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(login: any): Promise<JSON> {
|
||||||
|
return await api_client.get(`/user/${login}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async current(): Promise<JSON> {
|
||||||
|
return await api_client.get("/user/current");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new User();
|
@ -1,10 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Meerkat from '@/components/icons/Meerkat.vue';
|
import Meerkat from '@/components/icons/Meerkat.vue';
|
||||||
import NavBar from '@/components/NavBar.vue';
|
import NavBar from '@/components/NavBar.vue';
|
||||||
|
import User from "@/services/user";
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
function next() {
|
const user = ref(null);
|
||||||
window.location.pathname = "/me";
|
|
||||||
};
|
onMounted(async () => {
|
||||||
|
await User.current()
|
||||||
|
.then(async response => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
return Promise.reject(response.data && response.data.message || response.status);
|
||||||
|
};
|
||||||
|
if (response.data.hasOwnProperty("user")) {
|
||||||
|
user.value = response.data.user;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error("Error occured:", e);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -14,10 +29,11 @@ function next() {
|
|||||||
<Meerkat />
|
<Meerkat />
|
||||||
</template>
|
</template>
|
||||||
<template #right>
|
<template #right>
|
||||||
<RouterLink class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600" to="/user/login">
|
<RouterLink v-if="user" class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600"
|
||||||
|
:to="{ name: 'User', params: { user: user.login } }">{{ user.name }}</RouterLink>
|
||||||
|
<RouterLink v-if="!user" class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600"
|
||||||
|
to="/user/login">
|
||||||
Sign In</RouterLink>
|
Sign In</RouterLink>
|
||||||
<a class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600" href="/user/register">Sign
|
|
||||||
up</a>
|
|
||||||
</template>
|
</template>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
|
|
||||||
|
10
crates/frontend/src/views/Error.vue
Normal file
10
crates/frontend/src/views/Error.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Base from "@/views/Base.vue";
|
||||||
|
import Error from "@/components/Error.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Base>
|
||||||
|
<Error>Not Found</Error>
|
||||||
|
</Base>
|
||||||
|
</template>
|
@ -1,47 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Base from '@/views/Base.vue';
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const email = ref(null);
|
|
||||||
const name = ref(null);
|
|
||||||
const is_admin = ref(null);
|
|
||||||
const errorMessage = ref(null);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const asd = await fetch(import.meta.hot ? "http://localhost:54600/api/v1/user/profile" : "/api/v1/user/profile", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
credentials: "include",
|
|
||||||
})
|
|
||||||
.then(async response => {
|
|
||||||
const isJson = response.headers.get('content-type')?.includes('application/json');
|
|
||||||
const data = isJson && await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = (data && data.message) || response.status;
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
name.value = data.user.name;
|
|
||||||
email.value = data.user.email;
|
|
||||||
is_admin.value = data.user.is_admin;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
errorMessage.value = error;
|
|
||||||
console.error("Error occured:", error);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Base>
|
|
||||||
<p v-if="errorMessage" class="text-center pt-3 pb-3 bg-orange-900 rounded border border-orange-700">{{
|
|
||||||
errorMessage }}</p>
|
|
||||||
<p>{{ name }}</p>
|
|
||||||
<p>{{ email }}</p>
|
|
||||||
<p>{{ is_admin }}</p>
|
|
||||||
</Base>
|
|
||||||
</template>
|
|
54
crates/frontend/src/views/User.vue
Normal file
54
crates/frontend/src/views/User.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Base from '@/views/Base.vue';
|
||||||
|
import Error from "@/components/Error.vue";
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||||
|
import User from "@/services/user";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const name = ref(null);
|
||||||
|
const error = ref(null);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await User.get(route.params.user)
|
||||||
|
.then(async response => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
return Promise.reject(response.data && response.data.message || response.status);
|
||||||
|
};
|
||||||
|
if (response.data.hasOwnProperty("user")) {
|
||||||
|
name.value = response.data.user.name;
|
||||||
|
} else {
|
||||||
|
error.value = "404 Not Found";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error("Error occured:", e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => route.params.user, async (to, from) => {
|
||||||
|
await User.get(route.params.user)
|
||||||
|
.then(async response => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
return Promise.reject(response.data && response.data.message || response.status);
|
||||||
|
};
|
||||||
|
if (response.data.hasOwnProperty("user")) {
|
||||||
|
name.value = response.data.user.name;
|
||||||
|
} else {
|
||||||
|
error.value = "404 Not Found";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error("Error occured:", e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Base>
|
||||||
|
<div v-if="error">
|
||||||
|
<Error>{{ error }}</Error>
|
||||||
|
</div>
|
||||||
|
<p v-else>{{ name }}</p>
|
||||||
|
</Base>
|
||||||
|
</template>
|
@ -3,9 +3,10 @@ use std::sync::Arc;
|
|||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::{Request, State},
|
extract::{Request, State},
|
||||||
http::header,
|
http::{header, StatusCode},
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
|
Json,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ use crate::{db::user::User, state::AppState};
|
|||||||
use super::errors::AuthError;
|
use super::errors::AuthError;
|
||||||
use super::token::TokenClaims;
|
use super::token::TokenClaims;
|
||||||
|
|
||||||
pub async fn jwt_auth(
|
pub async fn jwt(
|
||||||
cookie_jar: CookieJar,
|
cookie_jar: CookieJar,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
mut req: Request<Body>,
|
mut req: Request<Body>,
|
||||||
@ -27,13 +28,8 @@ pub async fn jwt_auth(
|
|||||||
req.headers()
|
req.headers()
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|auth_header| auth_header.to_str().ok())
|
.and_then(|auth_header| auth_header.to_str().ok())
|
||||||
.and_then(|auth_value| {
|
.and_then(|auth_value| auth_value.strip_prefix("Bearer "))
|
||||||
if auth_value.starts_with("Bearer ") {
|
.map(|auth_token| auth_token.to_owned())
|
||||||
Some(auth_value[7..].to_owned())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let token = token.ok_or_else(|| AuthError::MissingToken)?;
|
let token = token.ok_or_else(|| AuthError::MissingToken)?;
|
||||||
@ -51,3 +47,28 @@ pub async fn jwt_auth(
|
|||||||
req.extensions_mut().insert(user);
|
req.extensions_mut().insert(user);
|
||||||
Ok(next.run(req).await)
|
Ok(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn jwt_auth(
|
||||||
|
cookie_jar: CookieJar,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
mut req: Request<Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
|
let token = cookie_jar
|
||||||
|
.get("token")
|
||||||
|
.map(|cookie| cookie.value().to_string())
|
||||||
|
.or_else(|| {
|
||||||
|
req.headers()
|
||||||
|
.get(header::AUTHORIZATION)
|
||||||
|
.and_then(|auth_header| auth_header.to_str().ok())
|
||||||
|
.and_then(|auth_value| auth_value.strip_prefix("Bearer "))
|
||||||
|
.map(|auth_token| auth_token.to_owned())
|
||||||
|
});
|
||||||
|
|
||||||
|
let user_id = token
|
||||||
|
.and_then(|token| TokenClaims::validate(token, state.config.jwt.secret.to_owned()).ok())
|
||||||
|
.and_then(|claims| uuid::Uuid::parse_str(&claims.sub).ok());
|
||||||
|
|
||||||
|
req.extensions_mut().insert(user_id);
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
||||||
|
@ -34,7 +34,11 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
|||||||
.route("/v1/user/remove", post(user::remove))
|
.route("/v1/user/remove", post(user::remove))
|
||||||
.route("/v1/user/login", post(user::login))
|
.route("/v1/user/login", post(user::login))
|
||||||
.route("/v1/user/logout", get(user::logout))
|
.route("/v1/user/logout", get(user::logout))
|
||||||
.route("/v1/user/profile", get(user::profile).route_layer(jwt))
|
.route(
|
||||||
|
"/v1/user/current",
|
||||||
|
get(user::current).route_layer(jwt.to_owned()),
|
||||||
|
)
|
||||||
|
.route("/v1/user/:login", get(user::profile).route_layer(jwt))
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.fallback(fallback)
|
.fallback(fallback)
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use argon2::Argon2;
|
use argon2::Argon2;
|
||||||
use argon2::{PasswordHash, PasswordVerifier};
|
use argon2::{PasswordHash, PasswordVerifier};
|
||||||
|
use axum::extract::Path;
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
@ -29,6 +30,7 @@ pub struct RegisterUser {
|
|||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct FilteredUser {
|
pub struct FilteredUser {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
pub login: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
@ -43,6 +45,7 @@ impl FilteredUser {
|
|||||||
pub fn from(user: &User) -> Self {
|
pub fn from(user: &User) -> Self {
|
||||||
FilteredUser {
|
FilteredUser {
|
||||||
id: user.id.to_string(),
|
id: user.id.to_string(),
|
||||||
|
login: user.login.to_string(),
|
||||||
name: user.name.to_owned(),
|
name: user.name.to_owned(),
|
||||||
email: user.email.to_owned(),
|
email: user.email.to_owned(),
|
||||||
is_admin: user.is_admin,
|
is_admin: user.is_admin,
|
||||||
@ -137,7 +140,7 @@ pub async fn login(
|
|||||||
.http_only(true);
|
.http_only(true);
|
||||||
|
|
||||||
let mut response =
|
let mut response =
|
||||||
Json(json!({"status": StatusCode::OK.to_string(), "token": token})).into_response();
|
Json(json!({"status": StatusCode::OK.to_string(), "token": token, "user": json!(FilteredUser::from(&user))})).into_response();
|
||||||
response
|
response
|
||||||
.headers_mut()
|
.headers_mut()
|
||||||
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
|
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
|
||||||
@ -162,9 +165,48 @@ pub async fn logout() -> Result<impl IntoResponse, (StatusCode, Json<serde_json:
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn profile(
|
pub async fn profile(
|
||||||
Extension(user): Extension<User>,
|
State(state): State<Arc<AppState>>,
|
||||||
|
Extension(user_id): Extension<Option<uuid::Uuid>>,
|
||||||
|
Path(login): Path<String>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
let user = User::find(&state.database, User::by_login(login))
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = if let Some(user) = user {
|
||||||
|
json!({"status": StatusCode::OK.to_string(), "user": json!(FilteredUser::from(&user))})
|
||||||
|
} else {
|
||||||
|
json!({"status": StatusCode::NOT_FOUND.to_string()})
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn current(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Extension(user_id): Extension<Option<uuid::Uuid>>,
|
||||||
|
) -> Result<impl IntoResponse, AuthError<impl std::error::Error>> {
|
||||||
|
let user = get_user(state, user_id).await?;
|
||||||
|
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
json!({"status":"success","user":json!(FilteredUser::from(&user))}),
|
json!({"status": StatusCode::OK.to_string(), "user": json!(FilteredUser::from(&user))}),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_user(
|
||||||
|
state: Arc<AppState>,
|
||||||
|
user_id: Option<uuid::Uuid>,
|
||||||
|
) -> Result<User, AuthError<impl std::error::Error>> {
|
||||||
|
let user = if let Some(user_id) = user_id {
|
||||||
|
User::find(&state.database, User::by_id(user_id))
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::InternalError)
|
||||||
|
} else {
|
||||||
|
Err(AuthError::InvalidCredentials)
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = user?.ok_or_else(|| AuthError::MissingUser)?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
@ -102,6 +102,13 @@ impl User {
|
|||||||
.filter(users::id.eq(id))
|
.filter(users::id.eq(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn by_login(login: String) -> BoxedQuery<'static> {
|
||||||
|
users::table
|
||||||
|
.into_boxed()
|
||||||
|
.select(User::as_select())
|
||||||
|
.filter(users::login.eq(login))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find(
|
pub async fn find(
|
||||||
pool: &Pool,
|
pool: &Pool,
|
||||||
query: BoxedQuery<'static>,
|
query: BoxedQuery<'static>,
|
||||||
|
Loading…
Reference in New Issue
Block a user