use std::fs;
use std::path::Path;
use crate::scaffold::{check_app, refuse_existing, write};
macro_rules! template {
($name:literal) => {
include_str!(concat!("../templates/access/", $name))
};
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Urls {
Session,
Nested,
Prefix,
}
impl Urls {
fn render(self, source: &str) -> String {
let (org_page, joined, enter) = match self {
Urls::Session => (
"org_path()",
"session.insert(\"org_id\", org_id)?\n redirect org_path(), notice: \"Welcome!\"",
"post \"/orgs/#{org.id}/switch\"\n \"/org\".to_s()",
),
Urls::Prefix => (
"org_path()",
"redirect \"/#{org_id}/org\", notice: \"Welcome!\"",
"\"/#{org.id}/org\"",
),
Urls::Nested => (
"org_path(org_access.org.id)",
"redirect org_path(org_id), notice: \"Welcome!\"",
"\"/orgs/#{org.id}\"",
),
};
let nested = self == Urls::Nested;
let path = |name: &str, org: &str, rest: &str| {
let args: Vec<&str> = [if nested { org } else { "" }, rest]
.into_iter()
.filter(|a| !a.is_empty())
.collect();
format!("{name}({})", args.join(", "))
};
source
.replace("{{org_page}}", org_page)
.replace("{{joined}}", joined)
.replace("{{enter_org}}", enter)
.replace(
"{{invitations_path}}",
&path("invitations_path", "org_id", ""),
)
.replace(
"{{new_invitation_path}}",
&path("new_invitation_path", "org.id", ""),
)
.replace(
"{{membership_path}}",
&path("membership_path", "org.id", "member.id"),
)
.replace(
"{{invitation_withdrawal_path}}",
&path("invitation_withdrawal_path", "org.id", "invitation.id"),
)
}
}
pub fn access(kind: &str, urls: Option<&str>) -> Result<(), String> {
if kind != "org" {
return Err(format!(
"`glacier generate access {kind}`: the kind there is is `org`"
));
}
check_app()?;
if !crate::scaffold::discovers() || !Path::new(crate::scaffold::ROUTES_GLACIER).exists() {
return Err(
"`glacier generate access` needs an app made with `glacier::app!()` and src/routes.glacier"
.into(),
);
}
if !Path::new("src/access/user_access.glacier").exists() {
return Err(
"run `glacier generate authentication` first: an org's access extends the user's"
.into(),
);
}
refuse_existing(&[
"src/models/org.glacier",
"src/models/membership.glacier",
"src/access/org_access.glacier",
"src/middleware/in_org.glacier",
"src/controllers/orgs_controller.glacier",
])?;
let api = crate::scaffold::api_app();
let urls = match (urls, api) {
(None, false) | (Some("session"), false) => Urls::Session,
(Some("session"), true) => {
return Err("a JSON API has no session to keep the org in: use `--urls nested`".into());
}
(None, true) | (Some("nested"), _) => Urls::Nested,
(Some("prefix"), false) => Urls::Prefix,
(Some("prefix"), true) => {
return Err(
"`--urls prefix` is for an app with pages: a JSON API uses `--urls nested`".into(),
);
}
(Some(other), _) => {
return Err(format!(
"`--urls {other}`: expected `session`, `nested` or `prefix`"
));
}
};
crate::generate::migration_with("create_orgs", template!("create_orgs.glacier"))?;
crate::generate::migration_with(
"create_memberships",
template!("create_memberships.glacier"),
)?;
write("src/models/org.glacier", template!("org.glacier"))?;
write(
"src/models/membership.glacier",
template!("membership.glacier"),
)?;
write(
"src/access/org_access.glacier",
template!("org_access.glacier"),
)?;
write(
"src/middleware/in_org.glacier",
match urls {
Urls::Session => template!("in_org_session.glacier"),
Urls::Prefix => template!("in_org_prefix.glacier"),
Urls::Nested => template!("in_org.glacier"),
},
)?;
if api {
write(
"src/controllers/orgs_controller.glacier",
template!("orgs_controller_api.glacier"),
)?;
write(
"tests/controllers/orgs_controller.test.glacier",
template!("orgs_controller_api.test.glacier"),
)?;
} else if urls == Urls::Prefix {
write(
"src/controllers/orgs_controller.glacier",
template!("orgs_controller_prefix.glacier"),
)?;
write(
"src/views/orgs/index.html",
template!("orgs_index_prefix.html"),
)?;
write("src/views/orgs/new.html", template!("orgs_new.html"))?;
write(
"tests/controllers/orgs_controller.test.glacier",
template!("orgs_controller_prefix.test.glacier"),
)?;
invitations(urls)?;
crate::scaffold::add_app_setting(
"middleware glacier::middleware::IdPrefix",
"URLs that start with an org's id, /42/…: the id comes off before routing.",
)?;
} else if urls == Urls::Session {
write(
"src/controllers/orgs_controller.glacier",
template!("orgs_controller_session.glacier"),
)?;
write("src/views/orgs/index.html", template!("orgs_index.html"))?;
write("src/views/orgs/new.html", template!("orgs_new.html"))?;
write(
"tests/controllers/orgs_controller.test.glacier",
template!("orgs_controller_session.test.glacier"),
)?;
invitations(urls)?;
} else {
write(
"src/controllers/orgs_controller.glacier",
template!("orgs_controller.glacier"),
)?;
write("src/views/orgs/new.html", template!("orgs_new.html"))?;
write(
"tests/controllers/orgs_controller.test.glacier",
template!("orgs_controller.test.glacier"),
)?;
invitations(urls)?;
}
write(
"tests/factories/org.glacier",
template!("org_factory.glacier"),
)?;
write(
"tests/factories/membership.glacier",
template!("membership_factory.glacier"),
)?;
add_routes(api, urls)?;
let scope = if urls != Urls::Nested {
"scope through `in_org` (the org's pages)"
} else {
"`/orgs/:org_id` scope, through `in_org`,"
};
println!(
"\nNext: glacier db migrate. An org's own routes go in the {scope} where\n\
`access` is the org's: give a model `owned_by :org` and read it with\n\
`Post.for(access)`, or `glacier generate scaffold Post title:string --access org`."
);
Ok(())
}
fn invitations(urls: Urls) -> Result<(), String> {
crate::generate::migration_with(
"create_invitations",
template!("create_invitations.glacier"),
)?;
write(
"src/models/invitation.glacier",
template!("invitation.glacier"),
)?;
write(
"src/policies/org_policy.glacier",
template!("org_policy.glacier"),
)?;
write(
"src/mailers/invitations_mailer.glacier",
template!("invitations_mailer.glacier"),
)?;
write(
"src/views/invitations_mailer/invite.txt",
template!("invitations_mailer_invite.txt"),
)?;
write(
"src/views/invitations_mailer/invite.html",
template!("invitations_mailer_invite.html"),
)?;
write(
"src/controllers/invitations_controller.glacier",
&urls.render(template!("invitations_controller.glacier")),
)?;
write(
"src/controllers/memberships_controller.glacier",
&urls.render(template!("memberships_controller.glacier")),
)?;
write(
"src/views/invitations/new.html",
&urls.render(template!("invitations_new.html")),
)?;
write(
"src/views/invitations/show.html",
template!("invitations_show.html"),
)?;
write(
"tests/controllers/invitations_controller.test.glacier",
&urls.render(template!("invitations_controller.test.glacier")),
)?;
write(
"src/views/orgs/show.html",
&urls.render(template!("orgs_show_members.html")),
)
}
fn add_routes(api: bool, urls: Urls) -> Result<(), String> {
let path = crate::scaffold::ROUTES_GLACIER;
let routes = fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
let mut updated = routes.clone();
let alias = " alias Middleware.InOrg\n";
if !updated.contains(alias) {
let anchor = " alias Middleware.RequireAuthentication\n";
updated = match updated.find(anchor) {
Some(at) => format!(
"{}{alias}{}",
&updated[..at + anchor.len()],
&updated[at + anchor.len()..]
),
None => match updated.find("routes do\n") {
Some(at) => {
let after = at + "routes do\n".len();
format!("{}{alias}{}", &updated[..after], &updated[after..])
}
None => format!("{alias}{updated}"),
},
};
}
let pipeline = " # Only an org's members, in its routes; see src/middleware/in_org.glacier.\n \
pipeline in_org = Pipeline.new().with(InOrg)\n";
let protect = if api { "api" } else { "browser" };
let making = if api {
" post \"/orgs\", OrgsController.create, as: :orgs\n".to_owned()
} else {
" get \"/orgs/new\", OrgsController.new, as: :new_org\n \
post \"/orgs\", OrgsController.create, as: :orgs\n"
.to_owned()
};
let switch = if urls == Urls::Session {
" post \"/orgs/:id/switch\", OrgsController.switch, as: :switch_org\n"
} else {
""
};
let at = if urls == Urls::Nested { "" } else { "/org" };
let (members, invitation_links) = if api {
(String::new(), "")
} else {
(
format!(
" get \"{at}/invitations/new\", InvitationsController.new, as: :new_invitation\n \
post \"{at}/invitations\", InvitationsController.create, as: :invitations\n \
delete \"{at}/invitations/:id\", InvitationsController.destroy, as: :invitation_withdrawal\n \
delete \"{at}/members/:id\", MembershipsController.destroy, as: :membership\n"
),
"\n # An invitation's link, for whoever is signed in (signing in comes back).\n \
scope \"/\", through: [:browser, :authenticated] do\n \
get \"/invitations/:token\", InvitationsController.show, as: :invitation\n \
post \"/invitations/:token\", InvitationsController.accept\n end\n",
)
};
let scope = if urls != Urls::Nested {
format!(
"\n scope \"/\", through: [:{protect}, :authenticated] do\n \
get \"/orgs\", OrgsController.index, as: :orgs\n \
get \"/orgs/new\", OrgsController.new, as: :new_org\n \
post \"/orgs\", OrgsController.create\n{switch} end\n\n \
# The org's pages: `access` in them is its OrgAccess.\n \
scope \"/\", through: [:{protect}, :authenticated, :in_org] do\n \
get \"/org\", OrgsController.show, as: :org\n{members} end\n{invitation_links}"
)
} else {
format!(
"\n scope \"/\", through: [:{protect}, :authenticated] do\n{making} end\n\n \
# An org's own pages: `access` in them is its OrgAccess.\n \
scope \"/orgs/:org_id\", through: [:{protect}, :authenticated, :in_org] do\n \
get \"/\", OrgsController.show, as: :org\n{members} end\n{invitation_links}"
)
};
let pipelines_at = updated
.rfind(" pipeline ")
.and_then(|at| updated[at..].find('\n').map(|end| at + end + 1));
let close = updated.rfind("\nend").map(|at| at + 1);
match (pipelines_at, close) {
(Some(pipelines_at), Some(close)) if pipelines_at < close => {
updated.insert_str(close, &scope);
updated.insert_str(pipelines_at, pipeline);
}
_ => println!("\nAdd these to the routes in {path}:\n\n{pipeline}{scope}"),
}
if updated != routes {
fs::write(path, updated).map_err(|e| format!("writing {path}: {e}"))?;
println!(" update {path}");
}
Ok(())
}