Skip to content

Commit 026abb0

Browse files
committed
claim_invite
1 parent ab3075d commit 026abb0

File tree

17 files changed

+228
-30
lines changed

17 files changed

+228
-30
lines changed

lib/api_auth/src/github.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use bencher_schema::{
77
context::ApiContext,
88
error::{issue_error, payment_required_error, unauthorized_error},
99
model::{
10-
organization::plan::LicenseUsage,
10+
organization::{plan::LicenseUsage, QueryOrganization},
1111
user::{InsertUser, QueryUser},
1212
},
1313
};
@@ -79,6 +79,11 @@ async fn post_inner(
7979
query_user.check_is_locked()?;
8080
if let Some(invite) = &json_oauth.invite {
8181
query_user.accept_invite(conn_lock!(context), &context.token_key, invite)?;
82+
} else if let Some(organization_uuid) = json_oauth.claim {
83+
let query_organization =
84+
QueryOrganization::from_uuid(conn_lock!(context), organization_uuid)?;
85+
let invite = query_organization.claim(context, &query_user).await?;
86+
query_user.accept_invite(conn_lock!(context), &context.token_key, &invite)?;
8287
}
8388
query_user
8489
} else {
@@ -88,6 +93,7 @@ async fn post_inner(
8893
email: email.clone(),
8994
plan: json_oauth.plan,
9095
invite: json_oauth.invite.clone(),
96+
claim: json_oauth.claim,
9197
i_agree: true,
9298
};
9399

lib/api_projects/src/claim.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use bencher_endpoint::{CorsResponse, Endpoint, Post, ResponseCreated};
2-
use bencher_json::{organization::member::OrganizationRole, JsonNewClaim, JsonProject, ResourceId};
2+
use bencher_json::{organization::member::OrganizationRole, JsonAccept, JsonNewClaim, ResourceId};
33
use bencher_schema::{
44
conn_lock,
55
context::ApiContext,
@@ -14,6 +14,10 @@ use dropshot::{endpoint, HttpError, Path, RequestContext, TypedBody};
1414
use schemars::JsonSchema;
1515
use serde::Deserialize;
1616

17+
// The claim token should be short-lived,
18+
// as it is meant to be used immediately after creation.
19+
const CLAIM_TOKEN_TTL: u32 = 60;
20+
1721
#[derive(Deserialize, JsonSchema)]
1822
pub struct ProjClaimParams {
1923
/// The slug or UUID for a project.
@@ -47,7 +51,7 @@ pub async fn proj_claim_post(
4751
bearer_token: BearerToken,
4852
path_params: Path<ProjClaimParams>,
4953
body: TypedBody<JsonNewClaim>,
50-
) -> Result<ResponseCreated<JsonProject>, HttpError> {
54+
) -> Result<ResponseCreated<JsonAccept>, HttpError> {
5155
let auth_user = AuthUser::from_token(rqctx.context(), bearer_token).await?;
5256
let json = post_inner(
5357
rqctx.context(),
@@ -64,7 +68,7 @@ async fn post_inner(
6468
path_params: ProjClaimParams,
6569
_json_claim: JsonNewClaim,
6670
auth_user: AuthUser,
67-
) -> Result<JsonProject, HttpError> {
71+
) -> Result<JsonAccept, HttpError> {
6872
let query_project = QueryProject::from_resource_id(conn_lock!(context), &path_params.project)?;
6973
let query_organization =
7074
QueryOrganization::get(conn_lock!(context), query_project.organization_id)?;
@@ -80,7 +84,7 @@ async fn post_inner(
8084
.token_key
8185
.new_invite(
8286
auth_user.email.clone(),
83-
60,
87+
CLAIM_TOKEN_TTL,
8488
query_organization.uuid,
8589
OrganizationRole::Leader,
8690
)
@@ -91,9 +95,6 @@ async fn post_inner(
9195
e,
9296
)
9397
})?;
94-
auth_user
95-
.user
96-
.accept_invite(conn_lock!(context), &context.token_key, &invite)?;
9798

98-
Ok(query_project.into_json_for_organization(conn_lock!(context), &query_organization))
99+
Ok(JsonAccept { invite })
99100
}

lib/bencher_json/src/system/auth.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bencher_valid::{PlanLevel, Secret};
55
use schemars::JsonSchema;
66
use serde::{Deserialize, Serialize};
77

8-
use crate::JsonUser;
8+
use crate::{JsonUser, OrganizationUuid};
99

1010
#[typeshare::typeshare]
1111
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -17,6 +17,7 @@ pub struct JsonSignup {
1717
#[cfg(feature = "plus")]
1818
pub plan: Option<PlanLevel>,
1919
pub invite: Option<Jwt>,
20+
pub claim: Option<OrganizationUuid>,
2021
/// I agree to the Bencher Terms of Use (https://bencher.dev/legal/terms-of-use), Privacy Policy (https://bencher.dev/legal/privacy), and License Agreement (https://bencher.dev/legal/license)
2122
pub i_agree: bool,
2223
}
@@ -40,6 +41,7 @@ pub struct JsonOAuth {
4041
#[cfg(feature = "plus")]
4142
pub plan: Option<PlanLevel>,
4243
pub invite: Option<Jwt>,
44+
pub claim: Option<OrganizationUuid>,
4345
}
4446

4547
#[typeshare::typeshare]

lib/bencher_schema/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
2020

2121
// TODO Custom max TTL
2222
pub const INVITE_TOKEN_TTL: u32 = u32::MAX;
23+
pub const CLAIM_TOKEN_TTL: u32 = 60;
2324

2425
#[derive(Debug, thiserror::Error)]
2526
pub enum MigrationError {

lib/bencher_schema/src/model/organization/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::{
2727
model::user::auth::AuthUser,
2828
resource_conflict_err, resource_not_found_err,
2929
schema::{self, organization as organization_table},
30-
ApiContext,
30+
ApiContext, CLAIM_TOKEN_TTL,
3131
};
3232

3333
use super::user::QueryUser;
@@ -241,6 +241,36 @@ impl QueryOrganization {
241241
QueryOrganizationRole::claimed_at(conn, self.id)
242242
}
243243

244+
pub async fn claim(
245+
&self,
246+
context: &ApiContext,
247+
query_user: &QueryUser,
248+
) -> Result<Jwt, HttpError> {
249+
if self.is_claimed(conn_lock!(context))? {
250+
return Err(unauthorized_error(format!(
251+
"This organization ({}) has already been claimed.",
252+
self.uuid
253+
)));
254+
}
255+
256+
// Create an invite token to claim the organization
257+
context
258+
.token_key
259+
.new_invite(
260+
query_user.email.clone(),
261+
CLAIM_TOKEN_TTL,
262+
self.uuid,
263+
OrganizationRole::Leader,
264+
)
265+
.map_err(|e| {
266+
issue_error(
267+
"Failed to create new claim token",
268+
"Failed to create new claim token.",
269+
e,
270+
)
271+
})
272+
}
273+
244274
pub fn into_json(self, conn: &mut DbConnection) -> JsonOrganization {
245275
let claimed = self.claimed_at(conn).ok();
246276
let Self {

lib/bencher_schema/src/model/user/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,16 @@ impl InsertUser {
227227

228228
let insert_org_role = if let Some(invite) = &json_signup.invite {
229229
InsertOrganizationRole::from_jwt(conn, token_key, invite, query_user.id)?
230+
} else if let Some(organization_uuid) = json_signup.claim {
231+
let organization_id = QueryOrganization::get_id(conn, organization_uuid)?;
232+
let timestamp = DateTime::now();
233+
InsertOrganizationRole {
234+
user_id: query_user.id,
235+
organization_id,
236+
role: OrganizationRole::Leader,
237+
created: timestamp,
238+
modified: timestamp,
239+
}
230240
} else {
231241
// Create an organization for the user
232242
let insert_organization = InsertOrganization::from_user(conn, &query_user);

services/api/openapi.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3880,7 +3880,7 @@
38803880
"content": {
38813881
"application/json": {
38823882
"schema": {
3883-
"$ref": "#/components/schemas/JsonProject"
3883+
"$ref": "#/components/schemas/JsonAccept"
38843884
}
38853885
}
38863886
}
@@ -9833,6 +9833,14 @@
98339833
"JsonOAuth": {
98349834
"type": "object",
98359835
"properties": {
9836+
"claim": {
9837+
"nullable": true,
9838+
"allOf": [
9839+
{
9840+
"$ref": "#/components/schemas/OrganizationUuid"
9841+
}
9842+
]
9843+
},
98369844
"code": {
98379845
"$ref": "#/components/schemas/Secret"
98389846
},
@@ -11223,6 +11231,14 @@
1122311231
"JsonSignup": {
1122411232
"type": "object",
1122511233
"properties": {
11234+
"claim": {
11235+
"nullable": true,
11236+
"allOf": [
11237+
{
11238+
"$ref": "#/components/schemas/OrganizationUuid"
11239+
}
11240+
]
11241+
},
1122611242
"email": {
1122711243
"$ref": "#/components/schemas/Email"
1122811244
},

services/cli/src/bencher/sub/system/auth/signup.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bencher_client::types::JsonSignup;
22
#[cfg(feature = "plus")]
33
use bencher_client::types::PlanLevel;
4-
use bencher_json::{Email, Jwt, Slug, UserName};
4+
use bencher_json::{Email, Jwt, OrganizationUuid, Slug, UserName};
55

66
use crate::{
77
bencher::{backend::PubBackend, sub::SubCmd},
@@ -17,6 +17,7 @@ pub struct Signup {
1717
#[cfg(feature = "plus")]
1818
pub plan: Option<PlanLevel>,
1919
pub invite: Option<Jwt>,
20+
pub claim: Option<OrganizationUuid>,
2021
pub i_agree: bool,
2122
pub backend: PubBackend,
2223
}
@@ -32,6 +33,7 @@ impl TryFrom<CliAuthSignup> for Signup {
3233
#[cfg(feature = "plus")]
3334
plan,
3435
invite,
36+
claim,
3537
i_agree,
3638
backend,
3739
} = signup;
@@ -42,6 +44,7 @@ impl TryFrom<CliAuthSignup> for Signup {
4244
#[cfg(feature = "plus")]
4345
plan: plan.map(Into::into),
4446
invite,
47+
claim,
4548
i_agree,
4649
backend: backend.try_into()?,
4750
})
@@ -57,6 +60,7 @@ impl From<Signup> for JsonSignup {
5760
#[cfg(feature = "plus")]
5861
plan,
5962
invite,
63+
claim,
6064
i_agree,
6165
..
6266
} = signup;
@@ -69,6 +73,7 @@ impl From<Signup> for JsonSignup {
6973
#[cfg(not(feature = "plus"))]
7074
plan: None,
7175
invite: invite.map(Into::into),
76+
claim: claim.map(Into::into),
7277
i_agree,
7378
}
7479
}

services/cli/src/parser/system/auth.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bencher_json::{Email, Jwt, Slug, UserName};
1+
use bencher_json::{Email, Jwt, OrganizationUuid, Slug, UserName};
22
use clap::{Parser, Subcommand};
33

44
#[cfg(feature = "plus")]
@@ -40,6 +40,10 @@ pub struct CliAuthSignup {
4040
#[clap(long)]
4141
pub invite: Option<Jwt>,
4242

43+
/// Organization UUID
44+
#[clap(long, value_name = "UUID")]
45+
pub claim: Option<OrganizationUuid>,
46+
4347
/// I agree to the Bencher Terms of Use (https://bencher.dev/legal/terms-of-use), Privacy Policy (https://bencher.dev/legal/privacy), and License Agreement (https://bencher.dev/legal/license)
4448
#[clap(long, required = true)]
4549
pub i_agree: bool,

services/console/src/components/auth/AuthForm.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1+
import * as Sentry from "@sentry/astro";
12
import {
23
Show,
34
createEffect,
45
createMemo,
56
createResource,
67
createSignal,
78
} from "solid-js";
8-
9-
import * as Sentry from "@sentry/astro";
109
import { createStore } from "solid-js/store";
1110
import {
1211
type JsonLogin,
1312
type JsonSignup,
1413
type Jwt,
1514
PlanLevel,
15+
type Uuid,
1616
} from "../../types/bencher";
1717
import { httpPost } from "../../util/http";
1818
import { NotifyKind, navigateNotify, pageNotify } from "../../util/notify";
1919
import { useSearchParams } from "../../util/url";
2020
import { init_valid, validJwt, validPlanLevel } from "../../util/valid";
2121
import Field, { type FieldHandler } from "../field/Field";
2222
import FieldKind from "../field/kind";
23-
import { AUTH_FIELDS, EMAIL_PARAM, INVITE_PARAM, PLAN_PARAM } from "./auth";
23+
import {
24+
AUTH_FIELDS,
25+
CLAIM_PARAM,
26+
EMAIL_PARAM,
27+
INVITE_PARAM,
28+
PLAN_PARAM,
29+
} from "./auth";
2430

2531
export interface Props {
2632
apiUrl: string;
@@ -37,6 +43,7 @@ const AuthForm = (props: Props) => {
3743

3844
const plan = () => searchParams[PLAN_PARAM]?.trim() as PlanLevel;
3945
const invite = () => searchParams[INVITE_PARAM]?.trim() as Jwt;
46+
const claim = () => searchParams[CLAIM_PARAM]?.trim() as Uuid;
4047
const [form, setForm] = createStore(initForm());
4148
const [submitting, setSubmitting] = createSignal(false);
4249
const [valid, setValid] = createSignal(false);
@@ -73,6 +80,7 @@ const AuthForm = (props: Props) => {
7380
setSubmitting(true);
7481
const plan_level = plan();
7582
const invite_token = invite();
83+
const claim_uuid = claim();
7684

7785
let authForm: JsonAuthForm;
7886
if (props.newUser) {
@@ -81,6 +89,9 @@ const AuthForm = (props: Props) => {
8189
email: form?.email?.value?.trim(),
8290
i_agree: form?.consent?.value,
8391
};
92+
if (claim_uuid) {
93+
signup.claim = claim_uuid;
94+
}
8495
authForm = signup;
8596
if (!plan_level) {
8697
setSearchParams({ [PLAN_PARAM]: PlanLevel.Free });

0 commit comments

Comments
 (0)