• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / backend / 5203125b39ec8db67efb51989a9fa8eae24b1a54

16 Nov 2024 11:39AM UTC coverage: 93.661% (-0.1%) from 93.78%
5203125b39ec8db67efb51989a9fa8eae24b1a54

push

github

web-flow
Transfer groundControl (and admin panel) from staff to admin route (#1180)

* Create a new staff scope

* Move Admin Panel requests into admin scope

* Change appropriate routes into admin scope

* Find-replace galore

* Fix linting

* Linting does not work :(

* Revert "Find-replace galore"

This reverts commit e77aa0505.

* Revert "Change appropriate routes into admin scope"

This reverts commit 18dc689a4.

* Revert "Create a new staff scope"

This reverts commit 6b7e54e98.

* Move dangerous routes into a new scope

* Fix linting

* Linting works in mysterious ways

* One more formatting change

* Swap order of all-staff and admin-only routes

This swap prevents the all-staff route,
"/grading/:submissionid/:questionid", from pattern
matching and overshadowing the admin-only route
"/grading/:assessmentid/publish_all_grades". Thankfully, no admin routes
overshadow staff routes, so a quick fix can be done here.

* Update error message for grading routes

* Update error messages for users

* Add test cases for assets for staff

Create test cases to indicate that non-admin staff can only read assets,
but not create, modify, or delete them.

* Update test auth to admin for assets

* Update and add tests for course config routes

Updates positive test auth from staff to admin, adds negative tests to
ensure that non-admin staff are unable to read, update, create, or
delete course configs.

* Update and add tests for assessment-level routes

Update the modification / deletion test auth from staff to admin, and
create tests to ensure that non-admin staff are not able to delete /
unpublish them

* Fix sourcecast error

* Revert "Fix sourcecast error"

This reverts commit 831ca601d.

* Transfer asset routes to admin

* Revert accidental formatting changes

9 of 9 new or added lines in 1 file covered. (100.0%)

4 existing lines in 2 files now uncovered.

3103 of 3313 relevant lines covered (93.66%)

1065.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.61
/lib/cadet_web/admin_controllers/admin_user_controller.ex
1
defmodule CadetWeb.AdminUserController do
2
  use CadetWeb, :controller
3
  use PhoenixSwagger
4

5
  import Ecto.Query
6

7
  alias Cadet.Repo
8
  alias Cadet.{Accounts, Assessments, Courses}
9
  alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role}
10

11
  # This controller is used to find all users of a course
12

13
  def index(conn, filter) do
14
    users =
3✔
15
      filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg)
3✔
16

17
    render(conn, "users.json", users: users)
3✔
18
  end
19

20
  def combined_total_xp(conn, %{"course_reg_id" => course_reg_id}) do
21
    course_reg = Repo.get(CourseRegistration, course_reg_id)
1✔
22

23
    course_id = course_reg.course_id
1✔
24
    user_id = course_reg.user_id
1✔
25
    course_reg_id = course_reg.id
1✔
26

27
    total_xp = Assessments.user_total_xp(course_id, user_id, course_reg_id)
1✔
28
    json(conn, %{totalXp: total_xp})
1✔
29
  end
30

31
  @add_users_role ~w(admin)a
32
  def get_students(conn, filter) do
33
    users =
×
34
      filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg)
×
35

36
    render(conn, "get_students.json", users: users)
×
37
  end
38

39
  @add_users_role ~w(admin)a
40
  def upsert_users_and_groups(conn, %{
41
        "course_id" => course_id,
42
        "users" => usernames_roles_groups,
43
        "provider" => provider
44
      }) do
45
    %{role: admin_role} = conn.assigns.course_reg
10✔
46
    usernames_roles_groups = usernames_roles_groups |> Enum.map(&to_snake_case_atom_keys/1)
10✔
47

48
    with {:validate_cap, true} <-
10✔
49
           {:validate_cap,
50
            Enum.count(CourseRegistrations.get_users(course_id) ++ usernames_roles_groups) <= 1500},
51
         {:validate_role, true} <- {:validate_role, admin_role in @add_users_role},
9✔
52
         {:validate_provider, true} <-
8✔
53
           {:validate_provider,
54
            Map.has_key?(Application.get_env(:cadet, :identity_providers, %{}), provider)},
55
         {:validate_usernames, true} <-
7✔
56
           {:validate_usernames,
57
            Enum.all?(usernames_roles_groups, fn x ->
58
              Map.has_key?(x, :username) and is_binary(x.username) and x.username != ""
37✔
59
            end)},
60
         {:validate_roles, true} <-
4✔
61
           {:validate_roles,
62
            Enum.all?(usernames_roles_groups, fn x ->
63
              Map.has_key?(x, :role) and String.to_atom(x.role) in Role.__enums__()
20✔
64
            end)} do
65
      {:ok, conn} =
2✔
66
        Repo.transaction(
67
          fn ->
68
            with {:upsert_users, :ok} <-
2✔
69
                   {:upsert_users,
70
                    CourseRegistrations.upsert_users_in_course(
71
                      provider,
72
                      usernames_roles_groups,
73
                      course_id
74
                    )},
75
                 {:upsert_groups, :ok} <-
2✔
76
                   {:upsert_groups,
77
                    Courses.upsert_groups_in_course(usernames_roles_groups, course_id, provider)} do
78
              text(conn, "OK")
2✔
79
            else
80
              {:upsert_users, {:error, {status, message}}} ->
81
                conn |> put_status(status) |> text(message)
×
82

83
              {:upsert_groups, {:error, {status, message}}} ->
84
                conn |> put_status(status) |> text(message)
×
85
            end
86
          end,
87
          timeout: 20_000
88
        )
89

90
      conn
2✔
91
    else
92
      {:validate_cap, false} ->
93
        conn |> put_status(:bad_request) |> text("A course can have maximum of 1500 users")
1✔
94

95
      {:validate_role, false} ->
96
        conn |> put_status(:forbidden) |> text("User is not permitted to add users")
1✔
97

98
      {:validate_provider, false} ->
99
        conn |> put_status(:bad_request) |> text("Invalid authentication provider")
1✔
100

101
      {:validate_usernames, false} ->
102
        conn |> put_status(:bad_request) |> text("Invalid username(s) provided")
3✔
103

104
      {:validate_roles, false} ->
105
        conn |> put_status(:bad_request) |> text("Invalid role(s) provided")
2✔
106
    end
107
  end
108

109
  @update_role_roles ~w(admin)a
110
  def update_role(conn, %{"role" => role, "course_reg_id" => course_reg_id}) do
111
    course_reg_id = course_reg_id |> String.to_integer()
6✔
112

113
    %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} =
6✔
114
      conn.assigns.course_reg
6✔
115

116
    with {:validate_role, true} <- {:validate_role, admin_role in @update_role_roles},
6✔
117
         {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != course_reg_id},
6✔
118
         {:get_cr, user_course_reg} when not is_nil(user_course_reg) <-
6✔
119
           {:get_cr, CourseRegistration |> where(id: ^course_reg_id) |> Repo.one()},
6✔
120
         {:validate_same_course, true} <-
5✔
121
           {:validate_same_course, user_course_reg.course_id == admin_course_id} do
5✔
122
      case CourseRegistrations.update_role(role, course_reg_id) do
4✔
123
        {:ok, %{}} ->
124
          text(conn, "OK")
3✔
125

126
        {:error, {status, message}} ->
127
          conn
128
          |> put_status(status)
129
          |> text(message)
1✔
130
      end
131
    else
132
      {:validate_role, false} ->
UNCOV
133
        conn |> put_status(:forbidden) |> text("User is not permitted to change others' roles")
×
134

135
      {:validate_not_self, false} ->
136
        conn |> put_status(:bad_request) |> text("Admin not allowed to downgrade own role")
×
137

138
      {:get_cr, _} ->
139
        conn |> put_status(:bad_request) |> text("User course registration does not exist")
1✔
140

141
      {:validate_same_course, false} ->
142
        conn |> put_status(:forbidden) |> text("User is in a different course")
1✔
143
    end
144
  end
145

146
  @delete_user_roles ~w(admin)a
147
  def delete_user(conn, %{"course_reg_id" => course_reg_id}) do
148
    course_reg_id = course_reg_id |> String.to_integer()
6✔
149

150
    %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} =
6✔
151
      conn.assigns.course_reg
6✔
152

153
    with {:validate_role, true} <- {:validate_role, admin_role in @delete_user_roles},
6✔
154
         {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != course_reg_id},
6✔
155
         {:get_cr, user_course_reg} when not is_nil(user_course_reg) <-
5✔
156
           {:get_cr, CourseRegistration |> where(id: ^course_reg_id) |> Repo.one()},
5✔
157
         {:prevent_delete_admin, true} <- {:prevent_delete_admin, user_course_reg.role != :admin},
4✔
158
         {:validate_same_course, true} <-
3✔
159
           {:validate_same_course, user_course_reg.course_id == admin_course_id} do
3✔
160
      case CourseRegistrations.delete_course_registration(course_reg_id) do
2✔
161
        {:ok, %{}} ->
162
          text(conn, "OK")
2✔
163

164
        {:error, {status, message}} ->
165
          conn
166
          |> put_status(status)
167
          |> text(message)
×
168
      end
169
    else
170
      {:validate_role, false} ->
UNCOV
171
        conn |> put_status(:forbidden) |> text("User is not permitted to delete other users")
×
172

173
      {:validate_not_self, false} ->
174
        conn
175
        |> put_status(:bad_request)
176
        |> text("Admin not allowed to delete ownself from course")
1✔
177

178
      {:get_cr, _} ->
179
        conn |> put_status(:bad_request) |> text("User course registration does not exist")
1✔
180

181
      {:prevent_delete_admin, false} ->
182
        conn |> put_status(:bad_request) |> text("Admins cannot be deleted")
1✔
183

184
      {:validate_same_course, false} ->
185
        conn |> put_status(:forbidden) |> text("User is in a different course")
1✔
186
    end
187
  end
188

189
  swagger_path :index do
1✔
190
    get("/courses/{course_id}/admin/users")
191

192
    summary("Returns a list of users in the course owned by the admin")
193

194
    security([%{JWT: []}])
195
    produces("application/json")
196
    response(200, "OK", Schema.ref(:AdminUserInfo))
197
    response(401, "Unauthorised")
198
  end
199

200
  swagger_path :combined_total_xp do
1✔
201
    get("/courses/{course_id}/admin/users/{course_reg_id}/total_xp")
202

203
    summary("Get the specified user's total XP from achievements and assessments")
204

205
    security([%{JWT: []}])
206
    produces("application/json")
207

208
    parameters do
209
      course_id(:path, :integer, "Course ID", required: true)
210
      course_reg_id(:path, :integer, "Course registration ID", required: true)
211
    end
212

213
    response(200, "OK", Schema.ref(:TotalXPInfo))
214
    response(401, "Unauthorised")
215
  end
216

217
  swagger_path :upsert_users_and_groups do
1✔
218
    put("/courses/{course_id}/admin/users")
219

220
    summary("Adds the list of usernames and roles to the course")
221
    security([%{JWT: []}])
222
    consumes("application/json")
223

224
    parameters do
225
      course_id(:path, :integer, "Course ID", required: true)
226
      users(:body, Schema.array(:UsernameAndRole), "Array of usernames and roles", required: true)
227

228
      provider(:body, :string, "The authentication provider linked to these usernames",
229
        required: true
230
      )
231
    end
232

233
    response(200, "OK")
234
    response(400, "Bad Request. Invalid provider, username or role")
235
    response(403, "Forbidden. You are not an admin")
236
  end
237

238
  swagger_path :update_role do
1✔
239
    put("/courses/{course_id}/admin/users/{course_reg_id}/role")
240

241
    summary("Updates the role of the given user in the the course")
242
    security([%{JWT: []}])
243
    consumes("application/json")
244

245
    parameters do
246
      course_id(:path, :integer, "Course ID", required: true)
247
      role(:body, :role, "The new role", required: true)
248

249
      courseRegId(
250
        :body,
251
        :integer,
252
        "The course registration of the user whose role is to be updated",
253
        required: true
254
      )
255
    end
256

257
    response(200, "OK")
258

259
    response(
260
      400,
261
      "Bad Request. User course registration does not exist or admin not allowed to downgrade own role"
262
    )
263

264
    response(403, "Forbidden. User is in different course, or you are not an admin")
265
  end
266

267
  swagger_path :delete_user do
1✔
268
    delete("/courses/{course_id}/admin/users/{course_reg_id}")
269

270
    summary("Deletes a user from a course")
271
    consumes("application/json")
272

273
    parameters do
274
      course_id(:path, :integer, "Course ID", required: true)
275

276
      courseRegId(
277
        :body,
278
        :integer,
279
        "The course registration of the user whose role is to be updated",
280
        required: true
281
      )
282
    end
283

284
    response(200, "OK")
285

286
    response(
287
      400,
288
      "Bad Request. User course registration does not exist or admin not allowed to delete ownself from course or admins cannot be deleted"
289
    )
290

291
    response(403, "Forbidden. User is in different course, or you are not an admin")
292
  end
293

294
  def swagger_definitions do
295
    %{
1✔
296
      AdminUserInfo:
297
        swagger_schema do
1✔
298
          title("User")
299
          description("Basic information about the user in this course")
300

301
          properties do
1✔
302
            userId(:integer, "User's ID")
303
            name(:string, "Full name of the user")
304

305
            role(
306
              :string,
307
              "Role of the user in this course. Can be 'student', 'staff', or 'admin'"
308
            )
309

310
            group(
1✔
311
              :string,
312
              "Group the user belongs to in this course. May be null if the user does not belong to any group"
313
            )
314
          end
315
        end,
316
      UsernameAndRole:
317
        swagger_schema do
1✔
318
          title("Username and role")
319
          description("Username and role of the user to add to this course")
320

321
          properties do
1✔
322
            username(:string, "The user's username")
323
            role(:role, "The user's role. Can be 'student', 'staff', or 'admin'")
1✔
324
          end
325
        end
326
    }
327
  end
328
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc