feat: session token and dev ease

This commit is contained in:
Fu Zi Xiang 2023-10-10 12:30:57 +08:00
parent 4a407ada33
commit a293bd34ee
No known key found for this signature in database
11 changed files with 159 additions and 70 deletions

1
admin_frontend/dev.env Normal file
View File

@ -0,0 +1 @@
GOTRUE_URL=http://localhost:9998

View File

@ -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`

View File

@ -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;
}
}
}

View File

@ -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())
} }
} }

View File

@ -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 };

View File

@ -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,
}

View File

@ -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,

View File

@ -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))
} }

View File

@ -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", ""))
}

View File

@ -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>

View File

@ -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