Skip to content

Commit

Permalink
CDC #2 - Moving users, projects, and user_projects into core_data_con…
Browse files Browse the repository at this point in the history
…nector
  • Loading branch information
dleadbetter committed Sep 10, 2023
1 parent 963d0d0 commit 9b824a3
Show file tree
Hide file tree
Showing 22 changed files with 440 additions and 7 deletions.
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.2.2
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem 'faker', '~> 3.2.1'
gem 'resource_api', git: 'https://github.com/performant-software/resource-api.git', tag: 'v0.4.5'
gem 'resource_api', git: 'https://github.com/performant-software/resource-api.git', tag: 'v0.5.1'

gem 'sqlite3'

Expand Down
15 changes: 13 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
GIT
remote: https://github.com/performant-software/resource-api.git
revision: ebfc4357bdfd13ae863f1e1aebdcc637be2ff6de
tag: v0.4.5
revision: d3e6e72abbd37e6bddd7ab461e37cf34d7c701ba
tag: v0.5.1
specs:
resource_api (0.1.0)
pagy (~> 5.10)
pundit (~> 2.3.1)
rails (>= 7.0, < 8)

PATH
remote: .
specs:
core_data_connector (0.1.0)
faker
rails (>= 6.0.3.2, < 8)
resource_api

Expand Down Expand Up @@ -87,6 +89,8 @@ GEM
crass (1.0.6)
date (3.3.3)
erubi (1.12.0)
faker (3.2.1)
i18n (>= 1.8.11, < 2)
globalid (1.1.0)
activesupport (>= 5.0)
i18n (1.14.1)
Expand All @@ -113,10 +117,14 @@ GEM
net-smtp (0.3.3)
net-protocol
nio4r (2.5.9)
nokogiri (1.15.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.15.3-x86_64-darwin)
racc (~> 1.4)
pagy (5.10.1)
activesupport
pundit (2.3.1)
activesupport (>= 3.0.0)
racc (1.7.1)
rack (2.2.8)
rack-test (2.1.0)
Expand Down Expand Up @@ -150,6 +158,7 @@ GEM
thor (~> 1.0)
zeitwerk (~> 2.5)
rake (13.0.6)
sqlite3 (1.6.3-arm64-darwin)
sqlite3 (1.6.3-x86_64-darwin)
thor (1.2.2)
timeout (0.4.0)
Expand All @@ -161,10 +170,12 @@ GEM
zeitwerk (2.6.11)

PLATFORMS
arm64-darwin-22
x86_64-darwin-19

DEPENDENCIES
core_data_connector!
faker (~> 3.2.1)
resource_api!
sqlite3

Expand Down
19 changes: 19 additions & 0 deletions app/controllers/core_data_connector/projects_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module CoreDataConnector
class ProjectsController < ApplicationController
# Search attributes
search_attributes :name

protected

# Automatically add the user who created the project as the owner, if they are not an admin.
def after_create(project)
return if current_user.admin?

UserProject.create(
project_id: project.id,
user_id: current_user.id,
role: UserProject::ROLE_OWNER
)
end
end
end
29 changes: 29 additions & 0 deletions app/controllers/core_data_connector/user_projects_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module CoreDataConnector
class UserProjectsController < ApplicationController
# Search attributes
search_attributes 'users.name', 'users.email', 'projects.name', 'projects.description'

# Joins
joins :user, :project

# Preloads
preloads :user, :project

protected

def base_query
return super if params[:id].present?

query = super

# User projects are only visible in the context of a user or a project.
if params[:project_id].present?
query.where(project_id: params[:project_id])
elsif params[:user_id].present?
query.where(user_id: params[:user_id])
else
query.none
end
end
end
end
5 changes: 5 additions & 0 deletions app/controllers/core_data_connector/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module CoreDataConnector
class UsersController < ApplicationController
search_attributes :name, :email
end
end
6 changes: 6 additions & 0 deletions app/models/core_data_connector/project.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module CoreDataConnector
class Project < ApplicationRecord
# Relationships
has_many :user_projects, dependent: :destroy
end
end
12 changes: 12 additions & 0 deletions app/models/core_data_connector/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module CoreDataConnector
class User < ApplicationRecord
# Relationships
has_many :user_projects, dependent: :destroy

# JWT
has_secure_password

# Validations
validates :email, uniqueness: true
end
end
56 changes: 56 additions & 0 deletions app/models/core_data_connector/user_project.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module CoreDataConnector
class UserProject < ApplicationRecord
# Constants
ROLE_OWNER = 'owner'
ROLE_EDITOR = 'editor'
ALLOWED_ROLES = [
ROLE_OWNER,
ROLE_EDITOR
]

# Relationships
belongs_to :user
belongs_to :project

# Transient attributes
attr_accessor :name, :email, :password, :password_confirmation

# Validations
validates :role, inclusion: { in: ALLOWED_ROLES, message: I18n.t('errors.user_project.roles') }
validates :user_id, uniqueness: { scope: :project_id, message: I18n.t('errors.user_project.unique') }

# Callbacks
before_update :reset_password
before_validation :find_or_create_user, on: :create

private

# Create the user record at the same time if the correct attributes are provided and no user_id is set. We'll
# only update the name and password if the user is a new record.
def find_or_create_user
return unless user_id.nil? && name.present? && email.present? && password.present? && password_confirmation.present?

user = User.find_or_create_by(email: email) do |user|
next unless user.new_record?

user.assign_attributes(
name: name,
password: password,
password_confirmation: password_confirmation
)
end

self.user_id = user.id
end

# Reset the user's password if the password and password confirmation attributes are provided
def reset_password
return unless user_id.present? && password.present? && password_confirmation.present?

user.update(
password: password,
password_confirmation: password_confirmation
)
end
end
end
77 changes: 77 additions & 0 deletions app/policies/core_data_connector/project_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module CoreDataConnector
class ProjectPolicy < BasePolicy
attr_reader :current_user, :project

def initialize(current_user, project)
@current_user = current_user
@project = project
end

# Any user can create a new project.
def create?
true
end

# A user can view any project for which they are a member.
def show?
return true if current_user.admin?

project_member?
end

# A user can delete a project if they are an admin or an owner of the project.
def destroy?
return true if current_user.admin?

project_owner?
end

# A user can update a project if they are an admin or an owner of the project.
def update?
return true if current_user.admin?

project_owner?
end

# Allowed create/update attributes.
def permitted_attributes
[:name, :description]
end

private

# Returns a query to find user_projects records for the passed user_id and project_id.
def project_member?
user_projects.exists?
end

# Returns a query to find user_projects records with an owner role for the passed user_id and project_id.
def project_owner?
user_projects
.where(role: UserProject::ROLE_OWNER)
.exists?
end

# Returns a query to find user_projects records for the current user and project.
def user_projects
current_user
.user_projects
.where(project_id: project.id)
end

# Admin users can view all projects. Non-admin users can view projects for which they are members.
class Scope < BaseScope
def resolve
return scope.all if current_user.admin?

scope.where(
UserProject
.where(user_id: current_user.id)
.where(UserProject.arel_table[:project_id].eq(Project.arel_table[:id]))
.arel
.exists
)
end
end
end
end
70 changes: 70 additions & 0 deletions app/policies/core_data_connector/user_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module CoreDataConnector
class UserPolicy < BasePolicy
attr_reader :current_user, :user

def initialize(current_user, user)
@current_user = current_user
@user = user
end

# Only admin users can create users directly.
def create?
current_user.admin?
end

# Only admin users can delete a user.
def destroy?
# Users cannot delete themselves, not even an admin
return false if current_user.id == user.id

current_user.admin?
end

# Only admin users can view users outside the context of a project. Users can view themselves outside the
# context of a project.
def show?
return true if current_user.admin?

current_user.id == user.id
end

# Only admin users can update users outside the context of a project. Users can update themselves outside the
# context of a project.
def update?
return true if current_user.admin?

current_user.id == user.id
end

# Allowed create/update attributes.
def permitted_attributes
params = [:name, :email, :password, :password_confirmation]
params << :admin if current_user.admin?
params
end

# A user can view another user if they have access to the same project.
class Scope < BaseScope
def resolve
return scope.all if current_user.admin?

user_projects = UserProject.arel_table.alias('b')

scope.where(
UserProject
.where(UserProject.arel_table[:user_id].eq(User.arel_table[:id]))
.where(
UserProject
.arel_table
.project(1)
.from(user_projects)
.where(user_projects[:project_id].eq(UserProject.arel_table[:project_id]))
.where(user_projects[:user_id].eq(current_user.id))
.exists
.to_sql
).arel.exists
)
end
end
end
end
Loading

0 comments on commit 9b824a3

Please sign in to comment.