Skip to content

Commit

Permalink
session management
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacharrisholt committed Sep 7, 2024
1 parent 5f35963 commit 62531ac
Show file tree
Hide file tree
Showing 10 changed files with 531 additions and 101 deletions.
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ gleam_pgo = ">= 0.13.0 and < 1.0.0"
birl = ">= 1.7.1 and < 2.0.0"
decode = ">= 0.2.0 and < 1.0.0"
gleam_json = ">= 1.0.1 and < 2.0.0"
youid = ">= 1.2.0 and < 2.0.0"
gleam_crypto = ">= 1.3.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
3 changes: 1 addition & 2 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,15 @@ packages = [
{ name = "startest", version = "0.5.0", build_tools = ["gleam"], requirements = ["argv", "bigben", "birl", "exception", "gleam_community_ansi", "gleam_erlang", "gleam_javascript", "gleam_stdlib", "glint", "simplifile", "tom"], otp_app = "startest", source = "hex", outer_checksum = "7A4BE0A1674D1DEB421B0BEB1E90B1A9C4AB21D954CF1754C0E28EA6B5DBE785" },
{ name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
{ name = "youid", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_stdlib"], otp_app = "youid", source = "hex", outer_checksum = "EF0F693004E221155EE5909C6D3C945DD14F7117DBA882887CF5F45BE399B8CA" },
]

[requirements]
argus = { version = ">= 1.0.0 and < 2.0.0" }
birl = { version = ">= 1.7.1 and < 2.0.0" }
decode = { version = ">= 0.2.0 and < 1.0.0" }
gleam_crypto = { version = ">= 1.3.0 and < 2.0.0" }
gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" }
gleam_json = { version = ">= 1.0.1 and < 2.0.0" }
gleam_pgo = { version = ">= 0.13.0 and < 1.0.0" }
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
youid = { version = ">= 1.2.0 and < 2.0.0" }
171 changes: 88 additions & 83 deletions src/pevensie/auth.gleam
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import argus
import birl.{type Time}
import birl/duration
import gleam/bit_array
import gleam/crypto.{Sha256}
import gleam/dict
import gleam/dynamic.{type Decoder}
import gleam/json
import gleam/io
import gleam/option.{type Option, None, Some}
import gleam/result
import pevensie/cache
import gleam/string
import pevensie/drivers.{
type AuthDriver, type Connected, type Disabled, type Disconnected,
}
import pevensie/internal/auth
import pevensie/internal/encoder.{type Encoder}
import pevensie/internal/pevensie.{type Pevensie}
import pevensie/internal/session.{type Session}
import pevensie/internal/user.{
type User as InternalUser, type UserInsert as UserInsertInternal,
type UserUpdate as UserUpdateInternal, Set, UserInsert, UserUpdate,
default_user_update,
}
import youid/uuid
import pevensie/net.{type IpAddress}

pub type User(user_metadata) =
InternalUser(user_metadata)
Expand All @@ -36,8 +37,14 @@ pub fn new_auth_config(
driver driver: AuthDriver(driver, user_metadata),
user_metadata_decoder user_metadata_decoder: Decoder(user_metadata),
user_metadata_encoder user_metadata_encoder: Encoder(user_metadata),
cookie_key cookie_key: String,
) -> AuthConfig(driver, user_metadata, Disconnected) {
auth.AuthConfig(driver, user_metadata_decoder, user_metadata_encoder)
auth.AuthConfig(
driver:,
user_metadata_decoder:,
user_metadata_encoder:,
cookie_key:,
)
}

pub fn disabled() -> AuthConfig(Nil, user_metadata, Disabled) {
Expand Down Expand Up @@ -120,9 +127,10 @@ pub fn create_user_with_email(
user_metadata: user_metadata,
) -> Result(User(user_metadata), Nil) {
let assert auth.AuthConfig(
driver,
user_metadata_decoder,
user_metadata_encoder,
driver:,
user_metadata_decoder:,
user_metadata_encoder:,
..,
) = pevensie.auth_config

// TODO: Handle errors
Expand Down Expand Up @@ -161,6 +169,7 @@ pub fn set_user_role(
driver:,
user_metadata_decoder:,
user_metadata_encoder:,
..,
) = pevensie.auth_config

driver.update_user(
Expand All @@ -173,72 +182,29 @@ pub fn set_user_role(
)
}

pub type Session {
Session(id: String, user_id: String, created_at: Time, expires_at: Time)
}

fn encode_session(session: Session) -> json.Json {
json.object([
#("id", json.string(session.id)),
#("user_id", json.string(session.user_id)),
#("created_at", json.string(birl.to_iso8601(session.created_at))),
#("expires_at", json.string(birl.to_iso8601(session.expires_at))),
])
}

fn session_decoder() -> Decoder(Session) {
let time_decoder = fn(time) {
use time <- result.try(dynamic.string(time))
birl.parse(time)
|> result.replace_error([])
}

dynamic.decode4(
Session,
dynamic.field("id", dynamic.string),
dynamic.field("user_id", dynamic.string),
dynamic.field("created_at", time_decoder),
dynamic.field("expires_at", time_decoder),
)
}

const session_resource_type = "pevensie:session"

pub fn create_session(
pevensie: Pevensie(
user_metadata,
auth_driver,
Connected,
cache_driver,
Connected,
cache_status,
),
user_id: String,
ip: Option(IpAddress),
user_agent: Option(String),
ttl_seconds: Option(Int),
) -> Result(Session, Nil) {
// Check if the user exists
use user <- result.try(get_user_by_id(pevensie, user_id))

let ttl_seconds = option.unwrap(ttl_seconds, 24 * 60 * 60)
let now = birl.now()
let session =
Session(
id: uuid.v7_string(),
user_id: user.id,
created_at: now,
expires_at: birl.add(now, duration.seconds(ttl_seconds)),
)
let assert auth.AuthConfig(driver, ..) = pevensie.auth_config

use _ <- result.try(cache.store(
pevensie,
session_resource_type,
session.id,
session
|> encode_session
|> json.to_string,
Some(ttl_seconds),
))

Ok(session)
driver.create_session(
driver.driver,
user_id,
ip,
user_agent,
ttl_seconds,
False,
)
}

pub fn get_session(
Expand All @@ -247,25 +213,13 @@ pub fn get_session(
auth_driver,
Connected,
cache_driver,
Connected,
cache_status,
),
session_id: String,
) -> Result(Option(Session), Nil) {
use session <- result.try(cache.get(
pevensie,
session_resource_type,
session_id,
))
case session {
None -> Ok(None)
Some(session_string) -> {
use decoded_session <- result.try(
json.decode(session_string, session_decoder())
|> result.replace_error(Nil),
)
Ok(Some(decoded_session))
}
}
let assert auth.AuthConfig(driver, ..) = pevensie.auth_config

driver.get_session(driver.driver, session_id, None, None)
}

pub fn delete_session(
Expand All @@ -278,8 +232,9 @@ pub fn delete_session(
),
session_id: String,
) -> Result(Nil, Nil) {
use _ <- result.try(cache.delete(pevensie, session_resource_type, session_id))
Ok(Nil)
let assert auth.AuthConfig(driver, ..) = pevensie.auth_config

driver.delete_session(driver.driver, session_id)
}

pub fn log_in_user(
Expand All @@ -292,12 +247,62 @@ pub fn log_in_user(
),
email: String,
password: String,
ip: Option(IpAddress),
user_agent: Option(String),
) -> Result(#(Session, User(user_metadata)), Nil) {
use user <- result.try(get_user_by_email_and_password(
pevensie,
email,
password,
))
create_session(pevensie, user.id, Some(24 * 60 * 60))
create_session(pevensie, user.id, ip, user_agent, Some(24 * 60 * 60))
|> result.map(fn(session) { #(session, user) })
}

fn sha256_hash(data: String, key: String) -> Result(String, Nil) {
let key = bit_array.from_string(key)
let data = bit_array.from_string(data)
Ok(crypto.sign_message(data, key, Sha256))
}

pub fn create_cookie(
pevensie: Pevensie(
user_metadata,
auth_driver,
Connected,
cache_driver,
cache_status,
),
session: Session,
) -> Result(String, Nil) {
let assert auth.AuthConfig(cookie_key:, ..) = pevensie.auth_config
io.println("Creating hash")
use hash <- result.try(sha256_hash(session.id, cookie_key))
io.println("Hash created")
Ok(session.id <> "|" <> hash)
}

// TODO: Improve errors
pub fn verify_cookie(
pevensie: Pevensie(
user_metadata,
auth_driver,
Connected,
cache_driver,
cache_status,
),
cookie: String,
) -> Result(String, Nil) {
let assert auth.AuthConfig(cookie_key:, ..) = pevensie.auth_config
let cookie_parts = string.split(cookie, "|")
case cookie_parts {
[session_id, hash_string] -> {
use new_hash <- result.try(sha256_hash(session_id, cookie_key))
case new_hash == hash_string {
True -> Ok(session_id)
False -> Error(Nil)
}
}
_ -> Error(Nil)
}
}
34 changes: 34 additions & 0 deletions src/pevensie/drivers.gleam
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import gleam/dynamic.{type Decoder}
import gleam/option.{type Option}
import pevensie/internal/encoder.{type Encoder}
import pevensie/internal/session.{type Session}
import pevensie/internal/user.{type User, type UserInsert, type UserUpdate}
import pevensie/net.{type IpAddress}

pub type Connected

Expand Down Expand Up @@ -64,6 +66,35 @@ type DeleteUserFunction(auth_driver, user_metadata) =
fn(auth_driver, String, String, Decoder(user_metadata)) ->
Result(User(user_metadata), Nil)

/// A function that gets a session by ID.
/// Args:
/// - auth_driver: The auth driver to use.
/// - session_id: The ID of the session to get.
/// - ip: The IP address of the user.
/// - user_agent: The user agent of the user.
type GetSessionFunction(auth_driver) =
fn(auth_driver, String, Option(IpAddress), Option(String)) ->
Result(Option(Session), Nil)

/// A function that creates a new session for a user.
/// Args:
/// - auth_driver: The auth driver to use.
/// - user_id: The ID of the user to create a session for.
/// - ip: The IP address of the user.
/// - user_agent: The user agent of the user.
/// - ttl_seconds: The number of seconds the session should last for.
/// - delete_other_sessions: Whether to delete any other sessions for the user.
type CreateSessionFunction(auth_driver) =
fn(auth_driver, String, Option(IpAddress), Option(String), Option(Int), Bool) ->
Result(Session, Nil)

/// A function that deletes a session by ID.
/// Args:
/// - auth_driver: The auth driver to use.
/// - session_id: The ID of the session to delete.
type DeleteSessionFunction(auth_driver) =
fn(auth_driver, String) -> Result(Nil, Nil)

pub type AuthDriver(driver, user_metadata) {
AuthDriver(
driver: driver,
Expand All @@ -73,6 +104,9 @@ pub type AuthDriver(driver, user_metadata) {
insert_user: InsertUserFunction(driver, user_metadata),
update_user: UpdateUserFunction(driver, user_metadata),
delete_user: DeleteUserFunction(driver, user_metadata),
get_session: GetSessionFunction(driver),
create_session: CreateSessionFunction(driver),
delete_session: DeleteSessionFunction(driver),
)
}

Expand Down
Loading

0 comments on commit 62531ac

Please sign in to comment.