Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ authors = ["stadust"]
description = "Libraries for creating pointercrate-like demonlist websites"
homepage = "https://pointercrate.com"
edition = "2021"
repository = "https://github.com/stadust/pointercrate"
repository = "https://github.com/stadust/pointercrate"
2 changes: 2 additions & 0 deletions migrations/20250308214833_google_account_id.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE members DROP COLUMN google_account_id;
2 changes: 2 additions & 0 deletions migrations/20250308214833_google_account_id.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE members ADD COLUMN google_account_id VARCHAR(256) UNIQUE;
18 changes: 18 additions & 0 deletions pointercrate-core-pages/src/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
self.with_script(Script::new(src))
}

fn async_script(self, src: impl Into<String>) -> Self {
self.with_script(Script::r#async(src))
}

Check warning on line 71 in pointercrate-core-pages/src/head.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/head.rs#L69-L71

Added lines #L69 - L71 were not covered by tests

fn module(self, module: impl Into<String>) -> Self {
self.with_script(Script::module(module))
}
Expand All @@ -85,20 +89,31 @@
pub struct Script {
src: String,
module: bool,
r#async: bool,
}

impl Script {
pub fn new<S: Into<String>>(src: S) -> Self {
Script {
src: src.into(),
module: false,
r#async: false,
}
}

Check warning on line 102 in pointercrate-core-pages/src/head.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/head.rs#L100-L102

Added lines #L100 - L102 were not covered by tests

pub fn r#async<S: Into<String>>(src: S) -> Self {
Script {
src: src.into(),
module: false,
r#async: true,

Check warning on line 108 in pointercrate-core-pages/src/head.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/head.rs#L104-L108

Added lines #L104 - L108 were not covered by tests
}
}

pub fn module<S: Into<String>>(src: S) -> Self {
Script {
src: src.into(),
module: true,
r#async: false,

Check warning on line 116 in pointercrate-core-pages/src/head.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/head.rs#L116

Added line #L116 was not covered by tests
}
}
}
Expand All @@ -109,6 +124,9 @@
@if self.module {
script src = (self.src) type = "module" {}
}
@else if self.r#async {

Check warning on line 127 in pointercrate-core-pages/src/head.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/head.rs#L127

Added line #L127 was not covered by tests
script src = (self.src) async {};
}
@else {
script src = (self.src) {};
}
Expand Down
49 changes: 3 additions & 46 deletions pointercrate-core-pages/static/css/ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,11 @@ textarea:invalid {
text-align: left;
font-style: italic;

margin: 2px 10px !important;
margin-top: 2px !important;
margin-bottom: 2px !important;
}

.form-input .error {
p.error {
font-size: 60% !important;
color: coral;
}
Expand Down Expand Up @@ -606,17 +607,6 @@ h2 .dropdown-menu ul {

/* (Under)lines */
.underlined {
margin-bottom: 5px;

/*border-bottom: 1px solid transparent;
border-image: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgba(211, 211, 211, 1) 15%,
rgba(211, 211, 211, 1) 85%,
rgba(0, 0, 0, 0) 100%
)
1;*/
border-bottom: 1px solid rgba(211, 211, 211, 1);
}

Expand All @@ -625,17 +615,6 @@ h2 .dropdown-menu ul {
}

.leftlined {
margin-left: 5px;

/*border-left: 1px solid transparent;
border-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(211, 211, 211, 1) 15%,
rgba(211, 211, 211, 1) 85%,
rgba(0, 0, 0, 0) 100%
)
1;*/
border-left: 1px solid rgba(211, 211, 211, 1);
}

Expand All @@ -644,17 +623,6 @@ h2 .dropdown-menu ul {
}

.rightlined {
margin-right: 5px;

/*border-right: 1px solid transparent;
border-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 0%,
rgba(211, 211, 211, 1) 15%,
rgba(211, 211, 211, 1) 85%,
rgba(0, 0, 0, 0) 100%
)
1;*/
border-right: 1px solid rgba(211, 211, 211, 1);
}

Expand All @@ -663,17 +631,6 @@ h2 .dropdown-menu ul {
}

.overlined {
margin-top: 5px;

/*border-top: 1px solid transparent;
border-image: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgba(211, 211, 211, 1) 15%,
rgba(211, 211, 211, 1) 85%,
rgba(0, 0, 0, 0) 100%
)
1;*/
border-top: 1px solid rgba(211, 211, 211, 1);
}

Expand Down
3 changes: 3 additions & 0 deletions pointercrate-example/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ pointercrate-user = { version = "0.1.0", path = "../pointercrate-user" }
pointercrate-user-api = { version = "0.1.0", path = "../pointercrate-user-api", features = ["legacy_accounts"] }
pointercrate-user-pages = { version = "0.1.0", path = "../pointercrate-user-pages", features = ["legacy_accounts"] }
rocket = "0.5.1"

[features]
oauth2 = ["pointercrate-user-api/oauth2", "pointercrate-user-pages/oauth2"]
29 changes: 29 additions & 0 deletions pointercrate-test/tests/user/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,32 @@ pub async fn test_login_no_header(pool: Pool<Postgres>) {
.execute()
.await;
}

#[sqlx::test(migrations = "../migrations")]
pub async fn test_no_login_if_google_account_linked(pool: Pool<Postgres>) {
let (client, mut connection) = pointercrate_test::user::setup_rocket(pool).await;

// Make sure the user we're trying to log in to exists
let user = pointercrate_test::user::system_user_with_perms(ADMINISTRATOR, &mut *connection).await;

client
.post("/api/v1/auth/", &())
.header("Authorization", "Basic UGF0cmljazpiYWQgcGFzc3dvcmQ=")
.header("X-Real-IP", "127.0.0.1")
.expect_status(Status::Ok)
.execute()
.await;

sqlx::query!("UPDATE members SET google_account_id='1' WHERE member_id=$1", user.user().id)
.execute(&mut *connection)
.await
.unwrap();

client
.post("/api/v1/auth/", &())
.header("Authorization", "Basic UGF0cmljazpiYWQgcGFzc3dvcmQ=")
.header("X-Real-IP", "127.0.0.1")
.expect_status(Status::Unauthorized)
.execute()
.await;
}
7 changes: 6 additions & 1 deletion pointercrate-user-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,10 @@ base64 = "0.22.1"
nonzero_ext = "0.3.0"
governor = "0.8.1"

# Dependencies needed only for oauth2
reqwest = { version = "0.12.12", optional = true, features = ["json"] }
chrono = { version = "0.4.40", optional = true }

[features]
legacy_accounts = ["pointercrate-user/legacy_accounts"]
legacy_accounts = ["pointercrate-user/legacy_accounts"]
oauth2 = ["pointercrate-user-pages/oauth2", "pointercrate-user/oauth2", "chrono", "reqwest"]
11 changes: 10 additions & 1 deletion pointercrate-user-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use rocket::{Build, Rocket};

pub mod auth;
mod endpoints;
#[cfg(feature = "oauth2")]
mod oauth;
mod pages;
mod ratelimits;

#[allow(unused_mut)]
pub fn setup(rocket: Rocket<Build>) -> Rocket<Build> {
pub fn setup(mut rocket: Rocket<Build>) -> Rocket<Build> {
let ratelimits = UserRatelimits::new();

let mut auth_routes = rocket::routes![
Expand All @@ -23,6 +25,13 @@ pub fn setup(rocket: Rocket<Build>) -> Rocket<Build> {
auth_routes.extend(rocket::routes![endpoints::auth::register]);
#[cfg(feature = "legacy_accounts")]
page_routes.extend(rocket::routes![pages::register]);
#[cfg(feature = "oauth2")]
auth_routes.extend(rocket::routes![pages::google_oauth_login]);

#[cfg(feature = "oauth2")]
{
rocket = rocket.manage(oauth::GoogleCertificateStore::default());
}

rocket
.manage(ratelimits)
Expand Down
83 changes: 83 additions & 0 deletions pointercrate-user-api/src/oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::sync::Arc;

use chrono::{Duration, Utc};
use pointercrate_user::auth::oauth::{GoogleCertificateDatabase, ValidatedGoogleCredentials};
use reqwest::Client;
use rocket::tokio::sync::RwLock;

pub const GOOGLE_CERT_ENDPOINT: &'static str = "https://www.googleapis.com/oauth2/v3/certs";

#[derive(Default)]
pub struct GoogleCertificateStore {
db: Arc<RwLock<GoogleCertificateDatabase>>,
}

#[allow(dead_code)] /* fields exist specifically for Debug output */
#[derive(Debug)]
pub enum CertificateRefreshError {
Reqwest(reqwest::Error),
MalformedCacheControlHeader(String),
MissingCacheControlHeader,
}

impl From<reqwest::Error> for CertificateRefreshError {
fn from(value: reqwest::Error) -> Self {
CertificateRefreshError::Reqwest(value)
}

Check warning on line 26 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L24-L26

Added lines #L24 - L26 were not covered by tests
}

impl GoogleCertificateStore {
pub async fn validate_credentials(&self, creds: &str) -> Option<ValidatedGoogleCredentials> {
self.db.read().await.validate_credentials(creds)
}

Check warning on line 32 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L30-L32

Added lines #L30 - L32 were not covered by tests

pub async fn needs_refresh(&self) -> bool {
self.db.read().await.needs_refresh()
}

Check warning on line 36 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L34-L36

Added lines #L34 - L36 were not covered by tests

pub async fn refresh(&self) -> Result<(), CertificateRefreshError> {
let mut guard = self.db.write().await;

Check warning on line 39 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L38-L39

Added lines #L38 - L39 were not covered by tests

// Between calling needs_refresh and us taking the write lock, did the need
// for a refresh potentially go away (e.g. because some other task did the refresh in the meantime)?
if !guard.needs_refresh() {
return Ok(());
}

let client = Client::new();

let request_time = Utc::now();
let response = client.get(GOOGLE_CERT_ENDPOINT).send().await?;

Check warning on line 50 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L43-L50

Added lines #L43 - L50 were not covered by tests

let cc_header = response
.headers()
.get("Cache-Control")
.ok_or(CertificateRefreshError::MissingCacheControlHeader)?;
let cc_header = cc_header
.to_str()
.map_err(|_| CertificateRefreshError::MalformedCacheControlHeader("unparsable".to_string()))?;

Check warning on line 58 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L52-L58

Added lines #L52 - L58 were not covered by tests

let max_age_directive = cc_header
.split(',')
.find(|directive| directive.trim().starts_with("max-age"))
.ok_or_else(|| CertificateRefreshError::MalformedCacheControlHeader(cc_header.to_string()))?;

Check warning on line 63 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L60-L63

Added lines #L60 - L63 were not covered by tests

let mut parts = max_age_directive.split('=');
_ = parts.next(); /* Split<'_, T> implements FusedIterator, so can ignore None handling here */
let age_str = parts
.next()
.ok_or_else(|| CertificateRefreshError::MalformedCacheControlHeader(max_age_directive.to_string()))?;
let age = age_str
.parse()
.map_err(|_| CertificateRefreshError::MalformedCacheControlHeader(age_str.to_string()))?;

Check warning on line 72 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L65-L72

Added lines #L65 - L72 were not covered by tests

let expiry = request_time + Duration::seconds(age);

Check warning on line 74 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L74

Added line #L74 was not covered by tests

let mut new_db = response.json::<GoogleCertificateDatabase>().await?;
new_db.expiry = Some(expiry);

*guard = new_db;

Ok(())
}

Check warning on line 82 in pointercrate-user-api/src/oauth.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-user-api/src/oauth.rs#L76-L82

Added lines #L76 - L82 were not covered by tests
}
Loading
Loading