feat: session token and dev ease
This commit is contained in:
parent
4a407ada33
commit
a293bd34ee
|
|
@ -0,0 +1 @@
|
||||||
|
GOTRUE_URL=http://localhost:9998
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Development
|
||||||
|
- Run the dev docker compose in the previous directory: `docker compose --file docker-compose-dev.yml up -d`
|
||||||
|
- cp `dev.env` to `.env`
|
||||||
|
- run the frontend: `cargo watch -x run -w .`
|
||||||
|
- web will be served at `localhost:3000`
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Minimal nginx configuration for AppFlowy-Cloud
|
||||||
|
# Self Hosted AppFlowy Cloud user should alter this file to suit their needs
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name gotrue;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:9998;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
# GoTrue
|
||||||
|
location ~ ^/(verify|authorize|callback|settings|user|token|admin) {
|
||||||
|
proxy_pass http://localhost:9998;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Admin Frontend
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
use axum::{response::IntoResponse, Json};
|
use axum::{http::status, response::IntoResponse};
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
// #[derive(serde::Serialize)]
|
||||||
pub struct WebApiError {
|
pub struct WebApiError {
|
||||||
pub code: i16,
|
pub status_code: status::StatusCode,
|
||||||
pub message: String,
|
pub payload: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebApiError {
|
impl WebApiError {
|
||||||
pub fn new(code: i16, message: String) -> Self {
|
pub fn new(code: status::StatusCode, message: String) -> Self {
|
||||||
Self { code, message }
|
Self {
|
||||||
|
status_code: code,
|
||||||
|
payload: message,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for WebApiError {
|
impl IntoResponse for WebApiError {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
Json(self).into_response()
|
(self.status_code, self.payload).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<gotrue_entity::GoTrueError> for WebApiError {
|
impl From<gotrue_entity::GoTrueError> for WebApiError {
|
||||||
fn from(v: gotrue_entity::GoTrueError) -> Self {
|
fn from(v: gotrue_entity::GoTrueError) -> Self {
|
||||||
WebApiError::new(500, v.to_string())
|
WebApiError::new(status::StatusCode::UNAUTHORIZED, v.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ async fn main() {
|
||||||
|
|
||||||
let gotrue_client = gotrue::api::Client::new(
|
let gotrue_client = gotrue::api::Client::new(
|
||||||
reqwest::Client::new(),
|
reqwest::Client::new(),
|
||||||
&std::env::var("GOTRUE_URL").unwrap_or("http://localhost:9999".to_string()),
|
&std::env::var("GOTRUE_URL").unwrap_or("http://gotrue:9999".to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let state = AppState { gotrue_client };
|
let state = AppState { gotrue_client };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
pub access_token: String,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,19 @@ where
|
||||||
pub data: T,
|
pub data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> WebApiResponse<T>
|
||||||
|
where
|
||||||
|
T: serde::Serialize,
|
||||||
|
{
|
||||||
|
pub fn new(data: T) -> Self {
|
||||||
|
Self {
|
||||||
|
code: 0,
|
||||||
|
message: "success".to_owned(),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> IntoResponse for WebApiResponse<T>
|
impl<T> IntoResponse for WebApiResponse<T>
|
||||||
where
|
where
|
||||||
T: serde::Serialize,
|
T: serde::Serialize,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use axum::response::Result;
|
|
||||||
use axum::{extract::State, routing::post, Json, Router};
|
|
||||||
use axum_extra::extract::cookie::Cookie;
|
|
||||||
use axum_extra::extract::CookieJar;
|
|
||||||
|
|
||||||
use crate::error::WebApiError;
|
use crate::error::WebApiError;
|
||||||
|
use crate::models::LoginResponse;
|
||||||
|
use crate::response::WebApiResponse;
|
||||||
use crate::{models::LoginRequest, AppState};
|
use crate::{models::LoginRequest, AppState};
|
||||||
|
use axum::response::Result;
|
||||||
|
use axum::Json;
|
||||||
|
use axum::{extract::State, routing::post, Router};
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
|
@ -17,10 +15,9 @@ pub fn router() -> Router<AppState> {
|
||||||
// TODO: Support OAuth2 login
|
// TODO: Support OAuth2 login
|
||||||
// login and set the cookie
|
// login and set the cookie
|
||||||
pub async fn login_handler(
|
pub async fn login_handler(
|
||||||
cookie_jar: CookieJar,
|
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(param): Json<LoginRequest>,
|
Json(param): Json<LoginRequest>,
|
||||||
) -> Result<CookieJar, WebApiError> {
|
) -> Result<WebApiResponse<LoginResponse>, WebApiError> {
|
||||||
let token = state
|
let token = state
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.token(&gotrue::grant::Grant::Password(
|
.token(&gotrue::grant::Grant::Password(
|
||||||
|
|
@ -31,12 +28,7 @@ pub async fn login_handler(
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(set_access_token_cookie(
|
Ok(WebApiResponse::new(LoginResponse {
|
||||||
cookie_jar,
|
access_token: token.access_token,
|
||||||
token.access_token.into(),
|
}))
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_access_token_cookie(jar: CookieJar, token: Cow<'static, str>) -> CookieJar {
|
|
||||||
jar.add(Cookie::new("access_token", token))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::error::RenderError;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::response::Result;
|
use axum::response::Result;
|
||||||
use axum::{response::Html, routing::get, Router};
|
use axum::{response::Html, routing::get, Router};
|
||||||
use axum_extra::extract::cookie::CookieJar;
|
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||||
|
|
||||||
use crate::templates;
|
use crate::templates;
|
||||||
|
|
||||||
|
|
@ -11,9 +11,14 @@ pub fn router() -> Router {
|
||||||
.route("/", get(home_handler))
|
.route("/", get(home_handler))
|
||||||
.route("/home", get(home_handler))
|
.route("/home", get(home_handler))
|
||||||
.route("/login", get(login_handler))
|
.route("/login", get(login_handler))
|
||||||
|
|
||||||
|
// for testing and debugging
|
||||||
|
.route("/setcookie", get(set_cookie_handler))
|
||||||
|
.route("/resetcookie", get(reset_cookie_handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn home_handler(cookies: CookieJar) -> Result<Html<String>, RenderError> {
|
pub async fn home_handler(cookies: CookieJar) -> Result<Html<String>, RenderError> {
|
||||||
|
println!("cookies: {:?}", cookies);
|
||||||
let access_token = cookies.get("access_token");
|
let access_token = cookies.get("access_token");
|
||||||
match access_token {
|
match access_token {
|
||||||
Some(access_token) => Ok(Html(access_token.to_string())), // TODO: render home page
|
Some(access_token) => Ok(Html(access_token.to_string())), // TODO: render home page
|
||||||
|
|
@ -25,3 +30,11 @@ pub async fn login_handler() -> Result<Html<String>, RenderError> {
|
||||||
let s = templates::Login {}.render()?;
|
let s = templates::Login {}.render()?;
|
||||||
Ok(Html(s))
|
Ok(Html(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_cookie_handler(jar: CookieJar) -> CookieJar {
|
||||||
|
jar.add(Cookie::new("access_token", "test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset_cookie_handler(jar: CookieJar) -> CookieJar {
|
||||||
|
jar.remove(Cookie::new("access_token", ""))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,62 @@
|
||||||
<html>
|
<html>
|
||||||
<script
|
|
||||||
src="https://unpkg.com/htmx.org@1.9.6"
|
|
||||||
integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
></script>
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Admin Login</h1>
|
<h1>Admin Login</h1>
|
||||||
<form>
|
<form id="loginForm">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<label for="email">Email:</label>
|
<label for="email">Email:</label>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" id="email" name="email" required/>
|
<input type="text" id="email" name="email" required />
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="password" id="password" name="password" required />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<button type="button" id="submitBtn">Submit</button>
|
||||||
|
</form>
|
||||||
|
<div id="response"></div>
|
||||||
|
|
||||||
</tr>
|
<script>
|
||||||
<tr>
|
document.getElementById('submitBtn').addEventListener('click', function () {
|
||||||
<td>
|
var data = {
|
||||||
<label for="password">Password:</label>
|
email: document.getElementById('email').value,
|
||||||
</td>
|
password: document.getElementById('password').value
|
||||||
<td>
|
};
|
||||||
<input type="password" id="password" name="password" required/>
|
fetch('/web-api/login', {
|
||||||
</td>
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
// If HTTP status code is not OK, throw an error with the status text
|
||||||
|
throw Error(response.statusText);
|
||||||
|
}
|
||||||
|
// Parse the JSON response
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Set the token as a cookie
|
||||||
|
document.cookie = "access_token=" + data.data.access_token + "; path=/";
|
||||||
|
window.location.href = "/home";
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
document.getElementById('response').innerText = 'Login failed: ' + error.message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<button hx-post="/web-api/login" hx-target="#response">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div id="response"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- appflowy_cloud
|
- appflowy_cloud
|
||||||
- gotrue
|
- gotrue
|
||||||
- admin_frontend
|
# - admin_frontend
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||||
- ./nginx/ssl/certificate.crt:/etc/nginx/ssl/certificate.crt
|
- ./nginx/ssl/certificate.crt:/etc/nginx/ssl/certificate.crt
|
||||||
|
|
@ -95,12 +95,12 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
|
|
||||||
admin_frontend:
|
#admin_frontend:
|
||||||
restart: on-failure
|
# restart: on-failure
|
||||||
build:
|
# build:
|
||||||
context: .
|
# context: .
|
||||||
dockerfile: ./admin_frontend/Dockerfile
|
# dockerfile: ./admin_frontend/Dockerfile
|
||||||
depends_on:
|
# depends_on:
|
||||||
- gotrue
|
# - gotrue
|
||||||
ports:
|
# ports:
|
||||||
- 3000:3000
|
# - 3000:3000
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue