set up /say endpoint
This commit is contained in:
parent
f7ca1c134b
commit
d84041d6e3
17 changed files with 345 additions and 82 deletions
|
@ -28,12 +28,16 @@ CREATE INDEX ON team_memberships (user_id);
|
||||||
|
|
||||||
CREATE TABLE api_keys (
|
CREATE TABLE api_keys (
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
team_id UUID NOT NULL REFERENCES teams(id)
|
team_id UUID NOT NULL REFERENCES teams(id),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_used_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
CREATE INDEX ON api_Keys (team_id);
|
CREATE INDEX ON api_Keys (team_id);
|
||||||
|
|
||||||
CREATE TABLE projects (
|
CREATE TABLE projects (
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
team_id UUID NOT NULL REFERENCES teams(id),
|
team_id UUID NOT NULL REFERENCES teams(id),
|
||||||
name TEXT NOT NULL
|
name TEXT NOT NULL,
|
||||||
|
UNIQUE (team_id, name)
|
||||||
);
|
);
|
||||||
|
CREATE INDEX ON projects(team_id);
|
||||||
|
|
1
migrations/2025-01-31-045014_messages/down.sql
Normal file
1
migrations/2025-01-31-045014_messages/down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS messages;
|
8
migrations/2025-01-31-045014_messages/up.sql
Normal file
8
migrations/2025-01-31-045014_messages/up.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id UUID PRIMARY KEY NOT NULL,
|
||||||
|
project_id UUID NOT NULL REFERENCES projects (id),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
message TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX ON messages (project_id);
|
||||||
|
CREATE INDEX ON messages (created_at);
|
|
@ -1,30 +1,34 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use deadpool_diesel::postgres::Connection;
|
use deadpool_diesel::postgres::Connection;
|
||||||
use diesel::prelude::*;
|
use diesel::{
|
||||||
|
dsl::{auto_type, AsSelect},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{app_error::AppError, schema::api_keys::dsl::*, teams::Team};
|
use crate::{app_error::AppError, schema::api_keys, teams::Team};
|
||||||
|
|
||||||
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = crate::schema::api_keys)]
|
#[diesel(table_name = api_keys)]
|
||||||
#[diesel(belongs_to(Team))]
|
#[diesel(belongs_to(Team))]
|
||||||
pub struct ApiKey {
|
pub struct ApiKey {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub team_id: Uuid,
|
pub team_id: Uuid,
|
||||||
|
pub last_used_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiKey {
|
impl ApiKey {
|
||||||
pub async fn generate_for_team(
|
pub async fn generate_for_team(db_conn: &Connection, team_id: Uuid) -> Result<Self, AppError> {
|
||||||
db_conn: &Connection,
|
|
||||||
key_team_id: Uuid,
|
|
||||||
) -> Result<Self, AppError> {
|
|
||||||
let api_key = Self {
|
let api_key = Self {
|
||||||
|
team_id,
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
team_id: key_team_id,
|
last_used_at: None,
|
||||||
};
|
};
|
||||||
let api_key_copy = api_key.clone();
|
let api_key_copy = api_key.clone();
|
||||||
db_conn
|
db_conn
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
diesel::insert_into(api_keys)
|
diesel::insert_into(api_keys::table)
|
||||||
.values(api_key_copy)
|
.values(api_key_copy)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
})
|
})
|
||||||
|
@ -32,4 +36,20 @@ impl ApiKey {
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
Ok(api_key)
|
Ok(api_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn all() -> _ {
|
||||||
|
let select: AsSelect<ApiKey, Pg> = ApiKey::as_select();
|
||||||
|
api_keys::table.select(select)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn with_id(id: Uuid) -> _ {
|
||||||
|
api_keys::id.eq(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn with_team(team_id: Uuid) -> _ {
|
||||||
|
api_keys::team_id.eq(team_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
match self {
|
match self {
|
||||||
Self::InternalServerError(err) => {
|
Self::InternalServerError(err) => {
|
||||||
tracing::error!("Application error: {}", err);
|
tracing::error!("Application error: {:?}", err);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response()
|
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response()
|
||||||
}
|
}
|
||||||
Self::ForbiddenError(client_message) => {
|
Self::ForbiddenError(client_message) => {
|
||||||
|
|
|
@ -4,6 +4,7 @@ mod app_state;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod csrf;
|
mod csrf;
|
||||||
mod guards;
|
mod guards;
|
||||||
|
mod messages;
|
||||||
mod projects;
|
mod projects;
|
||||||
mod router;
|
mod router;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
@ -12,6 +13,7 @@ mod settings;
|
||||||
mod team_memberships;
|
mod team_memberships;
|
||||||
mod teams;
|
mod teams;
|
||||||
mod users;
|
mod users;
|
||||||
|
mod v0_router;
|
||||||
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
|
42
src/messages.rs
Normal file
42
src/messages.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use diesel::{
|
||||||
|
dsl::{auto_type, AsSelect},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{projects::Project, schema::messages};
|
||||||
|
|
||||||
|
#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
|
||||||
|
#[diesel(table_name = messages)]
|
||||||
|
#[diesel(belongs_to(Project))]
|
||||||
|
pub struct Message {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub project_id: Uuid,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn all() -> _ {
|
||||||
|
let select: AsSelect<Message, Pg> = Message::as_select();
|
||||||
|
messages::table.select(select)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn with_project(project_id: Uuid) -> _ {
|
||||||
|
messages::project_id.eq(project_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn values_now(project_id: Uuid, message: String) -> _ {
|
||||||
|
let id: Uuid = Uuid::now_v7();
|
||||||
|
(
|
||||||
|
messages::id.eq(id),
|
||||||
|
messages::project_id.eq(project_id),
|
||||||
|
messages::message.eq(message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,13 +5,10 @@ use diesel::{
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{schema::projects, teams::Team};
|
||||||
schema::{self, projects::dsl::*},
|
|
||||||
teams::Team,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
#[derive(Associations, Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = schema::projects)]
|
#[diesel(table_name = projects)]
|
||||||
#[diesel(belongs_to(Team))]
|
#[diesel(belongs_to(Team))]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -23,6 +20,16 @@ impl Project {
|
||||||
#[auto_type(no_type_alias)]
|
#[auto_type(no_type_alias)]
|
||||||
pub fn all() -> _ {
|
pub fn all() -> _ {
|
||||||
let select: AsSelect<Project, Pg> = Project::as_select();
|
let select: AsSelect<Project, Pg> = Project::as_select();
|
||||||
projects.select(select)
|
projects::table.select(select)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn with_team(team_id: Uuid) -> _ {
|
||||||
|
projects::team_id.eq(team_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn with_name(name: String) -> _ {
|
||||||
|
projects::name.eq(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::Context;
|
||||||
use askama_axum::Template;
|
use askama_axum::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
|
@ -28,6 +29,7 @@ use crate::{
|
||||||
team_memberships::TeamMembership,
|
team_memberships::TeamMembership,
|
||||||
teams::Team,
|
teams::Team,
|
||||||
users::{CurrentUser, User},
|
users::{CurrentUser, User},
|
||||||
|
v0_router,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn new_router(state: AppState) -> Router<()> {
|
pub fn new_router(state: AppState) -> Router<()> {
|
||||||
|
@ -36,6 +38,7 @@ pub fn new_router(state: AppState) -> Router<()> {
|
||||||
format!("{}", base_path).as_str(),
|
format!("{}", base_path).as_str(),
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(landing_page))
|
.route("/", get(landing_page))
|
||||||
|
.nest("/v0", v0_router::new_router(state.clone()))
|
||||||
.route("/teams", get(teams_page))
|
.route("/teams", get(teams_page))
|
||||||
.route("/teams/{team_id}", get(team_page))
|
.route("/teams/{team_id}", get(team_page))
|
||||||
.route("/teams/{team_id}/projects", get(projects_page))
|
.route("/teams/{team_id}/projects", get(projects_page))
|
||||||
|
@ -64,17 +67,13 @@ async fn teams_page(
|
||||||
DbConn(conn): DbConn,
|
DbConn(conn): DbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let current_user_id = current_user.id.clone();
|
let team_memberships_query = current_user.clone().team_memberships();
|
||||||
let teams_of_current_user = conn
|
let teams: Vec<Team> = conn
|
||||||
.interact(move |conn| {
|
.interact(move |conn| team_memberships_query.load(conn))
|
||||||
schema::team_memberships::table
|
|
||||||
.inner_join(schema::teams::table)
|
|
||||||
.filter(schema::team_memberships::user_id.eq(current_user_id))
|
|
||||||
.select(Team::as_select())
|
|
||||||
.load(conn)
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()
|
||||||
|
.context("failed to load team memberships")
|
||||||
|
.map(|memberships| memberships.into_iter().map(|(_, team)| team).collect())?;
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "teams.html")]
|
#[template(path = "teams.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
@ -86,7 +85,7 @@ async fn teams_page(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
base_path,
|
base_path,
|
||||||
current_user,
|
current_user,
|
||||||
teams: teams_of_current_user,
|
teams,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
))
|
))
|
||||||
|
@ -129,6 +128,7 @@ async fn new_team_page(
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "new-team.html")]
|
#[template(path = "new-team.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
@ -197,16 +197,14 @@ async fn projects_page(
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
|
let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
|
||||||
|
|
||||||
let team_id = team.id.clone();
|
let api_keys_query = team.clone().api_keys();
|
||||||
let api_keys = db_conn
|
let (api_keys, projects) = db_conn
|
||||||
.interact(move |conn| {
|
.interact(move |conn| {
|
||||||
schema::api_keys::table
|
diesel::QueryResult::Ok((api_keys_query.load(conn)?, Project::all().load(conn)?))
|
||||||
.filter(schema::api_keys::team_id.eq(team_id))
|
|
||||||
.select(ApiKey::as_select())
|
|
||||||
.load(conn)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "projects.html")]
|
#[template(path = "projects.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
@ -223,9 +221,9 @@ async fn projects_page(
|
||||||
base_path,
|
base_path,
|
||||||
csrf_token,
|
csrf_token,
|
||||||
current_user,
|
current_user,
|
||||||
|
projects,
|
||||||
team,
|
team,
|
||||||
keys: api_keys,
|
keys: api_keys,
|
||||||
projects: vec![],
|
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,8 @@ diesel::table! {
|
||||||
api_keys (id) {
|
api_keys (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
team_id -> Uuid,
|
team_id -> Uuid,
|
||||||
|
created_at -> Timestamptz,
|
||||||
|
last_used_at -> Nullable<Timestamptz>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +26,15 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
messages (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
project_id -> Uuid,
|
||||||
|
created_at -> Timestamptz,
|
||||||
|
message -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
projects (id) {
|
projects (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
@ -57,6 +68,7 @@ diesel::table! {
|
||||||
|
|
||||||
diesel::joinable!(api_keys -> teams (team_id));
|
diesel::joinable!(api_keys -> teams (team_id));
|
||||||
diesel::joinable!(csrf_tokens -> users (user_id));
|
diesel::joinable!(csrf_tokens -> users (user_id));
|
||||||
|
diesel::joinable!(messages -> projects (project_id));
|
||||||
diesel::joinable!(projects -> teams (team_id));
|
diesel::joinable!(projects -> teams (team_id));
|
||||||
diesel::joinable!(team_memberships -> teams (team_id));
|
diesel::joinable!(team_memberships -> teams (team_id));
|
||||||
diesel::joinable!(team_memberships -> users (user_id));
|
diesel::joinable!(team_memberships -> users (user_id));
|
||||||
|
@ -65,6 +77,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
api_keys,
|
api_keys,
|
||||||
browser_sessions,
|
browser_sessions,
|
||||||
csrf_tokens,
|
csrf_tokens,
|
||||||
|
messages,
|
||||||
projects,
|
projects,
|
||||||
team_memberships,
|
team_memberships,
|
||||||
teams,
|
teams,
|
||||||
|
|
23
src/teams.rs
23
src/teams.rs
|
@ -1,14 +1,17 @@
|
||||||
use diesel::{
|
use diesel::{
|
||||||
dsl::{AsSelect, Select},
|
dsl::{auto_type, AsSelect, Eq},
|
||||||
pg::Pg,
|
pg::Pg,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::schema::teams::dsl::*;
|
use crate::{
|
||||||
|
api_keys::ApiKey,
|
||||||
|
schema::{api_keys, teams},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = crate::schema::teams)]
|
#[diesel(table_name = teams)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
pub struct Team {
|
pub struct Team {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -16,7 +19,17 @@ pub struct Team {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Team {
|
impl Team {
|
||||||
pub fn all() -> Select<teams, AsSelect<Team, Pg>> {
|
#[auto_type(no_type_alias)]
|
||||||
teams.select(Team::as_select())
|
pub fn all() -> _ {
|
||||||
|
let select: AsSelect<Team, Pg> = Team::as_select();
|
||||||
|
teams::table.select(select)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn api_keys(self) -> _ {
|
||||||
|
let all: diesel::dsl::Select<api_keys::table, AsSelect<ApiKey, Pg>> = ApiKey::all();
|
||||||
|
let id: Uuid = self.id;
|
||||||
|
let filter: Eq<api_keys::team_id, Uuid> = ApiKey::with_team(id);
|
||||||
|
all.filter(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
58
src/users.rs
58
src/users.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::FromRequestParts,
|
extract::FromRequestParts,
|
||||||
http::request::Parts,
|
http::request::Parts,
|
||||||
|
@ -7,17 +8,24 @@ use axum::{
|
||||||
use diesel::{
|
use diesel::{
|
||||||
associations::Identifiable,
|
associations::Identifiable,
|
||||||
deserialize::Queryable,
|
deserialize::Queryable,
|
||||||
dsl::{insert_into, AsSelect, Eq, Select},
|
dsl::{auto_type, insert_into, AsSelect, Eq, Select},
|
||||||
pg::Pg,
|
pg::Pg,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
Selectable,
|
Selectable,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{app_error::AppError, app_state::AppState, auth::AuthInfo, schema::users::dsl::*};
|
use crate::{
|
||||||
|
app_error::AppError,
|
||||||
|
app_state::AppState,
|
||||||
|
auth::AuthInfo,
|
||||||
|
schema::{team_memberships, teams, users},
|
||||||
|
team_memberships::TeamMembership,
|
||||||
|
teams::Team,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = crate::schema::users)]
|
#[diesel(table_name = users)]
|
||||||
#[diesel(check_for_backend(Pg))]
|
#[diesel(check_for_backend(Pg))]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -26,12 +34,24 @@ pub struct User {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub fn all() -> Select<users, AsSelect<User, Pg>> {
|
pub fn all() -> Select<users::table, AsSelect<User, Pg>> {
|
||||||
users.select(User::as_select())
|
users::table.select(User::as_select())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_uid(uid_value: &str) -> Eq<uid, &str> {
|
pub fn with_uid(uid_value: &str) -> Eq<users::uid, &str> {
|
||||||
uid.eq(uid_value)
|
users::uid.eq(uid_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn team_memberships(self) -> _ {
|
||||||
|
let user_id: Uuid = self.id.clone();
|
||||||
|
let user_id_filter: Eq<team_memberships::user_id, Uuid> =
|
||||||
|
TeamMembership::with_user_id(user_id);
|
||||||
|
let select: AsSelect<(TeamMembership, Team), Pg> = <(TeamMembership, Team)>::as_select();
|
||||||
|
team_memberships::table
|
||||||
|
.inner_join(teams::table)
|
||||||
|
.filter(user_id_filter)
|
||||||
|
.select(select)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,21 +78,37 @@ impl FromRequestParts<AppState> for CurrentUser {
|
||||||
let maybe_current_user = User::all()
|
let maybe_current_user = User::all()
|
||||||
.filter(User::with_uid(&auth_info.sub))
|
.filter(User::with_uid(&auth_info.sub))
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.optional()?;
|
.optional()
|
||||||
|
.context("failed to load maybe_current_user")?;
|
||||||
if let Some(current_user) = maybe_current_user {
|
if let Some(current_user) = maybe_current_user {
|
||||||
return Ok(current_user);
|
return Ok(current_user);
|
||||||
}
|
}
|
||||||
let new_user = User {
|
let new_user = User {
|
||||||
id: Uuid::now_v7(),
|
id: Uuid::now_v7(),
|
||||||
uid: auth_info.sub,
|
uid: auth_info.sub.clone(),
|
||||||
email: auth_info.email,
|
email: auth_info.email,
|
||||||
};
|
};
|
||||||
insert_into(users)
|
match insert_into(users::table)
|
||||||
.values(new_user)
|
.values(new_user)
|
||||||
.on_conflict(uid)
|
.on_conflict(users::uid)
|
||||||
.do_nothing()
|
.do_nothing()
|
||||||
.returning(User::as_returning())
|
.returning(User::as_returning())
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
|
{
|
||||||
|
QueryResult::Err(diesel::result::Error::NotFound) => {
|
||||||
|
tracing::debug!("detected race to insert current user record");
|
||||||
|
User::all()
|
||||||
|
.filter(User::with_uid(&auth_info.sub))
|
||||||
|
.first(conn)
|
||||||
|
.context(
|
||||||
|
"failed to load record after detecting race to insert current user",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
QueryResult::Err(err) => {
|
||||||
|
Err(err).context("failed to insert current user record")
|
||||||
|
}
|
||||||
|
QueryResult::Ok(result) => Ok(result),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
89
src/v0_router.rs
Normal file
89
src/v0_router.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::{
|
||||||
|
extract::Query,
|
||||||
|
response::{IntoResponse, Json},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use diesel::{dsl::insert_into, prelude::*, update};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api_keys::ApiKey,
|
||||||
|
app_error::AppError,
|
||||||
|
app_state::{AppState, DbConn},
|
||||||
|
messages::Message,
|
||||||
|
projects::Project,
|
||||||
|
schema::{api_keys, messages, projects},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn new_router(state: AppState) -> Router<AppState> {
|
||||||
|
Router::new().route("/say", get(say_get)).with_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SayQuery {
|
||||||
|
key: Uuid,
|
||||||
|
project: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn say_get(
|
||||||
|
DbConn(db_conn): DbConn,
|
||||||
|
Query(query): Query<SayQuery>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
let key = query.key.clone();
|
||||||
|
let maybe_api_key = db_conn
|
||||||
|
.interact(move |conn| {
|
||||||
|
update(api_keys::table.filter(ApiKey::with_id(key)))
|
||||||
|
.set(api_keys::last_used_at.eq(diesel::dsl::now))
|
||||||
|
.returning(ApiKey::as_returning())
|
||||||
|
.get_result(conn)
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.context("unable to get API key")?;
|
||||||
|
let api_key = match maybe_api_key {
|
||||||
|
Some(api_key) => api_key,
|
||||||
|
None => return Err(AppError::ForbiddenError("key not accepted".to_string())),
|
||||||
|
};
|
||||||
|
let project_name = query.project.to_lowercase();
|
||||||
|
let project = db_conn
|
||||||
|
.interact(move |conn| {
|
||||||
|
insert_into(projects::table)
|
||||||
|
.values((
|
||||||
|
projects::id.eq(Uuid::now_v7()),
|
||||||
|
projects::team_id.eq(api_key.team_id.clone()),
|
||||||
|
projects::name.eq(project_name.clone()),
|
||||||
|
))
|
||||||
|
.on_conflict((projects::team_id, projects::name))
|
||||||
|
.do_nothing()
|
||||||
|
.execute(conn)?;
|
||||||
|
// It would be nice to merge these two database operations into one,
|
||||||
|
// but it's not trivial to do so without faking an update; refer to:
|
||||||
|
// https://stackoverflow.com/a/42217872
|
||||||
|
Project::all()
|
||||||
|
.filter(Project::with_team(api_key.team_id))
|
||||||
|
.filter(Project::with_name(project_name))
|
||||||
|
.first(conn)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.context("unable to get project")?;
|
||||||
|
db_conn
|
||||||
|
.interact(move |conn| {
|
||||||
|
insert_into(messages::table)
|
||||||
|
.values(Message::values_now(project.id, query.message))
|
||||||
|
.execute(conn)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.context("unable to insert message")?;
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ResponseBody {
|
||||||
|
ok: bool,
|
||||||
|
}
|
||||||
|
Ok(Json(ResponseBody { ok: true }))
|
||||||
|
}
|
|
@ -20,6 +20,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#">Projects</a>
|
<a class="nav-link" href="#">Projects</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Channels</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 profile-menu">
|
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 profile-menu">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button class="btn btn-primary" type="submit">Submit</button>
|
<button class="btn btn-primary" type="submit">Submit</button>
|
||||||
<a class="btn btn-outline-light" role="button" href="{{ base_path }}/teams">
|
<a class="btn btn-secondary" role="button" href="{{ base_path }}/teams">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,11 +35,16 @@
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-3">
|
<section class="mb-3">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Project Name</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for project in projects %}
|
{% for project in projects %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ base_path }}/projects/{{ project.id }}">
|
<a href="{{ base_path }}/teams/{{ team.id.simple() }}/projects/{{ project.id.simple() }}">
|
||||||
{{ project.name }}
|
{{ project.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -64,10 +69,10 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
API key
|
API Key
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th title="Last Used (UTC)">
|
||||||
Last used
|
Last Used
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
Actions
|
Actions
|
||||||
|
@ -83,11 +88,22 @@
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
Unknown
|
{% if let Some(last_used_at) = key.last_used_at %}
|
||||||
|
{{ last_used_at.format("%Y-%m-%d") }}
|
||||||
|
{% else %}
|
||||||
|
Never
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm" role="group" aria-label="API key actions">
|
<div class="btn-group btn-group-sm" role="group" aria-label="API key actions">
|
||||||
<button class="btn btn-outline-light" type="button">Copy</button>
|
<button
|
||||||
|
class="btn btn-outline-light"
|
||||||
|
type="button"
|
||||||
|
name="api-key-copy-button"
|
||||||
|
data-copy="{{ key.id.simple() }}"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
<button class="btn btn-outline-light" type="button">Delete</button>
|
<button class="btn btn-outline-light" type="button">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -100,4 +116,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
document.getElementsByName("api-key-copy-button")
|
||||||
|
.forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function (ev) {
|
||||||
|
var content = ev.currentTarget.getAttribute("data-copy");
|
||||||
|
navigator.clipboard.writeText(content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -8,34 +8,34 @@
|
||||||
<li class="breadcrumb-item active" aria-current="page">Teams</li>
|
<li class="breadcrumb-item active" aria-current="page">Teams</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="mt-5">
|
<main class="container mt-5">
|
||||||
<section class="container mb-3">
|
<section class="mb-4">
|
||||||
<h1 class="mb-4">Teams</h1>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h1>Teams</h1>
|
||||||
|
<div>
|
||||||
|
<a href="{{ base_path }}/new-team" role="button" class="btn btn-primary">New Team</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% if teams.len() == 0 %}
|
{% if teams.len() == 0 %}
|
||||||
<div class="container">
|
<div class="alert alert-primary" role="alert">
|
||||||
<div class="alert alert-primary" role="alert">
|
Doesn't look like you've created or been invited to any teams yet.
|
||||||
Doesn't look like you've created or been invited to any teams yet.
|
|
||||||
<a href="{{ base_path }}/new-team">Click here</a> to create one.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<section class="mb-3">
|
<section class="mb-3">
|
||||||
<div class="container">
|
<table class="table">
|
||||||
<table class="table">
|
<tbody>
|
||||||
<tbody>
|
{% for team in teams %}
|
||||||
{% for team in teams %}
|
<tr>
|
||||||
<tr>
|
<td>
|
||||||
<td>
|
<a href="{{ base_path }}/teams/{{ team.id }}">
|
||||||
<a href="{{ base_path }}/teams/{{ team.id }}">
|
{{ team.name }}
|
||||||
{{ team.name }}
|
</a>
|
||||||
</a>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
{% endfor %}
|
||||||
{% endfor %}
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue