diff --git a/api/src/ids.rs b/api/src/ids.rs index 871a38c6..8dc9ef11 100644 --- a/api/src/ids.rs +++ b/api/src/ids.rs @@ -8,8 +8,9 @@ use thiserror::Error; /// A scope name, like `user` or `admin`. The name is not prefixed with an @. /// The name must be at least 2 characters long, and at most 20 characters long. -/// The name must only contain alphanumeric characters and hyphens. -/// The name must not start or end with a hyphen. +/// The name must only contain alphanumeric characters, hyphens, and dots. +/// The name must not start or end with a hyphen or dot. +/// The name must not contain consecutive hyphens or dots. #[derive(Clone, PartialEq, Eq, Hash)] pub struct ScopeName(String); @@ -26,7 +27,9 @@ impl ScopeName { if !name .chars() // temp allow underscores - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') + .all(|c| { + c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.' + }) { return Err(ScopeNameValidateError::InvalidCharacters); } @@ -35,10 +38,18 @@ impl ScopeName { return Err(ScopeNameValidateError::LeadingOrTrailingHyphens); } + if name.starts_with('.') || name.ends_with('.') { + return Err(ScopeNameValidateError::LeadingOrTrailingDots); + } + if name.contains("--") { return Err(ScopeNameValidateError::DoubleHyphens); } + if name.contains("..") { + return Err(ScopeNameValidateError::DoubleDots); + } + Ok(ScopeName(name)) } } @@ -135,15 +146,21 @@ pub enum ScopeNameValidateError { TooLong, #[error( - "scope name must contain only lowercase ascii alphanumeric characters and hyphens" + "scope name must contain only lowercase ascii alphanumeric characters, hyphens, and dots" )] InvalidCharacters, #[error("scope name must not start or end with a hyphen")] LeadingOrTrailingHyphens, + #[error("scope name must not start or end with a dot")] + LeadingOrTrailingDots, + #[error("scope name must not contain double hyphens")] DoubleHyphens, + + #[error("scope name must not contain double dots")] + DoubleDots, } /// A scope description, like 'This is a user scope' or 'Admin scope'. @@ -992,6 +1009,11 @@ mod tests { assert!(ScopeName::try_from("foo-123-bar").is_ok()); assert!(ScopeName::try_from("f123").is_ok()); assert!(ScopeName::try_from("foo-bar-baz-qux").is_ok()); + // Test valid scope names with dots + assert!(ScopeName::try_from("my.org").is_ok()); + assert!(ScopeName::try_from("foo.bar").is_ok()); + assert!(ScopeName::try_from("foo.bar.baz").is_ok()); + assert!(ScopeName::try_from("org.name-123").is_ok()); // Test invalid scope names assert!(ScopeName::try_from("").is_err()); @@ -1007,6 +1029,12 @@ mod tests { assert!(ScopeName::try_from("-123-foo").is_err()); assert!(ScopeName::try_from("foo-123-bar-").is_err()); assert!(ScopeName::try_from("@foo").is_err()); + // Test invalid scope names with dots + assert!(ScopeName::try_from(".foo").is_err()); + assert!(ScopeName::try_from("foo.").is_err()); + assert!(ScopeName::try_from("foo..bar").is_err()); + assert!(ScopeName::try_from(".org").is_err()); + assert!(ScopeName::try_from("test.").is_err()); } #[test] diff --git a/frontend/utils/ids.ts b/frontend/utils/ids.ts index 134ef45f..36dc8328 100644 --- a/frontend/utils/ids.ts +++ b/frontend/utils/ids.ts @@ -6,8 +6,14 @@ export const validateScopeName = (name: string) => { if (name.length < 3) { return "Name must be at least 3 characters."; } - if (!/^[a-zA-Z0-9-_]+$/.test(name)) { - return "Name can only contain letters, numbers, dashes, and underscores."; + if (!/^[a-z0-9-.]+$/.test(name)) { + return "Name can only contain lowercase letters, numbers, dashes, and dots."; + } + if (name.startsWith(".") || name.endsWith(".")) { + return "Name must not start or end with a dot."; + } + if (name.includes("..")) { + return "Name must not contain consecutive dots."; } return null; };