Add /api/v1/apps endpoint

This commit is contained in:
silverpill 2023-02-10 23:29:42 +00:00
parent 8958dca939
commit 2d9a43b076
13 changed files with 232 additions and 2 deletions

View file

@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Added `/api/v1/apps` endpoint.
- Documented `http_cors_allowlist` configuration parameter.
### Changed

View file

@ -584,6 +584,47 @@ paths:
$ref: '#/components/schemas/Relationship'
404:
description: Profile not found
/api/v1/apps:
post:
summary: Create a new application to obtain OAuth2 credentials.
requestBody:
content:
application/json:
schema:
type: object
properties:
client_name:
description: A name for your application.
type: string
redirect_uris:
description: Where the user should be redirected after authorization.
type: string
scopes:
description: Space separated list of scopes.
type: string
example: 'read write'
website:
description: An URL to the homepage of your app.
type: string
nullable: true
responses:
200:
description: Successful operation
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Application'
- type: object
properties:
client_id:
description: Client ID key, to be used for obtaining OAuth tokens.
type: string
client_secret:
description: Client secret key, to be used for obtaining OAuth tokens.
type: string
400:
description: Invalid request data.
/api/v1/custom_emojis:
get:
summary: Returns custom emojis that are available on the server.
@ -1360,6 +1401,19 @@ components:
type: string
enum:
- update
Application:
type: object
properties:
name:
description: The name of your application.
type: string
website:
description: The website associated with your application.
type: string
nullable: true
redirect_uri:
description: Where the user should be redirected after authorization.
type: string
Attachment:
type: object
properties:

View file

@ -0,0 +1,10 @@
CREATE TABLE oauth_application (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
app_name VARCHAR(100) NOT NULL,
website VARCHAR(100),
scopes VARCHAR(200) NOT NULL,
redirect_uri VARCHAR(200) NOT NULL,
client_id UUID UNIQUE NOT NULL,
client_secret VARCHAR(100) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View file

@ -51,6 +51,17 @@ CREATE TABLE user_account (
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
CREATE TABLE oauth_application (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
app_name VARCHAR(100) NOT NULL,
website VARCHAR(100),
scopes VARCHAR(200) NOT NULL,
redirect_uri VARCHAR(200) NOT NULL,
client_id UUID UNIQUE NOT NULL,
client_secret VARCHAR(100) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE oauth_token (
id SERIAL PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,

View file

@ -20,6 +20,7 @@ use mitra::http::json_error_handler;
use mitra::job_queue::scheduler;
use mitra::logger::configure_logger;
use mitra::mastodon_api::accounts::views::account_api_scope;
use mitra::mastodon_api::apps::views::application_api_scope;
use mitra::mastodon_api::custom_emojis::views::custom_emoji_api_scope;
use mitra::mastodon_api::directory::views::directory_api_scope;
use mitra::mastodon_api::instance::views::instance_api_scope;
@ -145,6 +146,7 @@ async fn main() -> std::io::Result<()> {
))
.service(oauth_api_scope())
.service(account_api_scope())
.service(application_api_scope())
.service(custom_emoji_api_scope())
.service(directory_api_scope())
.service(instance_api_scope())

View file

@ -0,0 +1,2 @@
mod types;
pub mod views;

View file

@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Deserialize)]
pub struct CreateAppRequest {
pub client_name: String,
pub redirect_uris: String,
pub scopes: String,
pub website: Option<String>,
}
/// https://docs.joinmastodon.org/entities/Application/
#[derive(Serialize)]
pub struct OauthApp {
pub name: String,
pub website: Option<String>,
pub redirect_uri: String,
pub client_id: Option<Uuid>,
pub client_secret: Option<String>,
}

View file

@ -0,0 +1,55 @@
use actix_web::{
post,
web,
Either,
HttpResponse,
Scope,
};
use uuid::Uuid;
use crate::database::{get_database_client, DbPool};
use crate::errors::HttpError;
use crate::mastodon_api::oauth::utils::generate_access_token;
use crate::models::{
oauth::queries::create_oauth_app,
oauth::types::DbOauthAppData,
};
use super::types::{OauthApp, CreateAppRequest};
/// https://docs.joinmastodon.org/methods/apps/
#[post("")]
async fn create_app_view(
db_pool: web::Data<DbPool>,
request_data: Either<
web::Json<CreateAppRequest>,
web::Form<CreateAppRequest>,
>,
) -> Result<HttpResponse, HttpError> {
let request_data = match request_data {
Either::Left(json) => json.into_inner(),
Either::Right(form) => form.into_inner(),
};
let db_client = &**get_database_client(&db_pool).await?;
let db_app_data = DbOauthAppData {
app_name: request_data.client_name,
website: request_data.website,
scopes: request_data.scopes,
redirect_uri: request_data.redirect_uris,
client_id: Uuid::new_v4(),
client_secret: generate_access_token(),
};
let db_app = create_oauth_app(db_client, db_app_data).await?;
let app = OauthApp {
name: db_app.app_name,
website: db_app.website,
redirect_uri: db_app.redirect_uri,
client_id: Some(db_app.client_id),
client_secret: Some(db_app.client_secret),
};
Ok(HttpResponse::Ok().json(app))
}
pub fn application_api_scope() -> Scope {
web::scope("/api/v1/apps")
.service(create_app_view)
}

View file

@ -1,4 +1,5 @@
pub mod accounts;
pub mod apps;
pub mod custom_emojis;
pub mod directory;
pub mod instance;

View file

@ -1,4 +1,4 @@
pub mod auth;
mod types;
pub mod views;
mod utils;
pub mod utils;

View file

@ -1 +1,2 @@
pub mod types;
pub mod queries;

View file

@ -1,9 +1,44 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::database::{DatabaseClient, DatabaseError};
use crate::database::{
catch_unique_violation,
DatabaseClient,
DatabaseError,
};
use crate::models::profiles::types::DbActorProfile;
use crate::models::users::types::{DbUser, User};
use super::types::{DbOauthApp, DbOauthAppData};
pub async fn create_oauth_app(
db_client: &impl DatabaseClient,
app_data: DbOauthAppData,
) -> Result<DbOauthApp, DatabaseError> {
let row = db_client.query_one(
"
INSERT INTO oauth_application (
app_name,
website,
scopes,
redirect_uri,
client_id,
client_secret
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING oauth_application
",
&[
&app_data.app_name,
&app_data.website,
&app_data.scopes,
&app_data.redirect_uri,
&app_data.client_id,
&app_data.client_secret,
],
).await.map_err(catch_unique_violation("oauth_application"))?;
let app = row.try_get("oauth_application")?;
Ok(app)
}
pub async fn save_oauth_token(
db_client: &impl DatabaseClient,
@ -94,6 +129,18 @@ mod tests {
use crate::models::users::types::UserCreateData;
use super::*;
#[tokio::test]
#[serial]
async fn test_create_oauth_app() {
let db_client = &create_test_database().await;
let db_app_data = DbOauthAppData {
app_name: "My App".to_string(),
..Default::default()
};
let app = create_oauth_app(db_client, db_app_data).await.unwrap();
assert_eq!(app.app_name, "My App");
}
#[tokio::test]
#[serial]
async fn test_delete_oauth_token() {

26
src/models/oauth/types.rs Normal file
View file

@ -0,0 +1,26 @@
use chrono::{DateTime, Utc};
use postgres_types::FromSql;
use uuid::Uuid;
#[derive(FromSql)]
#[postgres(name = "oauth_application")]
pub struct DbOauthApp {
pub id: i32,
pub app_name: String,
pub website: Option<String>,
pub scopes: String,
pub redirect_uri: String,
pub client_id: Uuid,
pub client_secret: String,
pub created_at: DateTime<Utc>,
}
#[cfg_attr(test, derive(Default))]
pub struct DbOauthAppData {
pub app_name: String,
pub website: Option<String>,
pub scopes: String,
pub redirect_uri: String,
pub client_id: Uuid,
pub client_secret: String,
}