Compare commits
2 Commits
f379bba89a
...
80715ba98b
| Author | SHA1 | Date | |
|---|---|---|---|
| 80715ba98b | |||
| ef12dffb62 |
@@ -18,6 +18,45 @@ steps:
|
|||||||
branch: release
|
branch: release
|
||||||
event: push
|
event: push
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
image: bitnami/kubectl:latest
|
||||||
|
volumes:
|
||||||
|
- /home/future2/Studio_v2/woodpecker/kube/config:/tmp/kubeconfig:ro
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
commit="$(printf %.8s "$CI_COMMIT_SHA")"
|
||||||
|
job="studio-auth-migrate-$commit"
|
||||||
|
kubectl --kubeconfig=/tmp/kubeconfig -n studio-system delete job "$job" --ignore-not-found
|
||||||
|
cat <<EOF | kubectl --kubeconfig=/tmp/kubeconfig apply -f -
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: $job
|
||||||
|
namespace: studio-system
|
||||||
|
spec:
|
||||||
|
backoffLimit: 1
|
||||||
|
ttlSecondsAfterFinished: 600
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: migrate
|
||||||
|
image: localhost:5000/studio-auth-be:0.1.1
|
||||||
|
imagePullPolicy: Always
|
||||||
|
command: ["sh", "-lc", "bunx --no-install prisma migrate deploy"]
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: studio-auth-be-env
|
||||||
|
EOF
|
||||||
|
if ! kubectl --kubeconfig=/tmp/kubeconfig -n studio-system wait --for=condition=complete "job/$job" --timeout=180s; then
|
||||||
|
kubectl --kubeconfig=/tmp/kubeconfig -n studio-system logs "job/$job" --all-containers=true || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
when:
|
||||||
|
branch: release
|
||||||
|
event: push
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
image: bitnami/kubectl:latest
|
image: bitnami/kubectl:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { randomInt } from 'crypto'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { config } from '../config'
|
import { config } from '../config'
|
||||||
import { sendEmailVerificationCode } from '../mail'
|
import { sendEmailVerificationCode } from '../mail'
|
||||||
|
import { prisma } from '../prisma'
|
||||||
import { verifyPassword } from '../security/password'
|
import { verifyPassword } from '../security/password'
|
||||||
import { signAccessToken, verifyAccessToken } from '../security/tokens'
|
import { signAccessToken, verifyAccessToken } from '../security/tokens'
|
||||||
import type { AuthUser } from '../types'
|
import type { AuthUser } from '../types'
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
findOrCreateOAuthUser,
|
findOrCreateOAuthUser,
|
||||||
findUserProfileImage,
|
findUserProfileImage,
|
||||||
findUserByEmail,
|
findUserByEmail,
|
||||||
|
findUserById,
|
||||||
listOrganizations,
|
listOrganizations,
|
||||||
isLoginBlocked,
|
isLoginBlocked,
|
||||||
markLastLogin,
|
markLastLogin,
|
||||||
@@ -153,6 +155,14 @@ const userResponse = (user: AuthUser) => ({
|
|||||||
|
|
||||||
const requireAdmin = (user: AuthUser): boolean => user.role === 'ADMIN'
|
const requireAdmin = (user: AuthUser): boolean => user.role === 'ADMIN'
|
||||||
|
|
||||||
|
const canManageLmsStudents = (user: AuthUser): boolean => (
|
||||||
|
user.role === 'ADMIN'
|
||||||
|
|| (user.appRoles ?? []).some((appRole) => (
|
||||||
|
appRole.appKey.toUpperCase() === 'LMS'
|
||||||
|
&& ['INSTRUCTOR', 'LMS_ADMIN'].includes(appRole.roleCode.toUpperCase())
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
const accessTokenResponse = async (user: AuthUser) => ({
|
const accessTokenResponse = async (user: AuthUser) => ({
|
||||||
accessToken: await signAccessToken(user),
|
accessToken: await signAccessToken(user),
|
||||||
accessTokenExpiresIn: config.accessTokenTtlSeconds,
|
accessTokenExpiresIn: config.accessTokenTtlSeconds,
|
||||||
@@ -802,6 +812,49 @@ authRoutes.post('/admin/users', authRequired, async (c) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
authRoutes.post('/instructor/students', authRequired, async (c) => {
|
||||||
|
const instructor = c.get('user')
|
||||||
|
if (!canManageLmsStudents(instructor)) return c.json({ error: 'forbidden' }, 403)
|
||||||
|
|
||||||
|
const body = adminCreateUserSchema.safeParse(await c.req.json().catch(() => null))
|
||||||
|
if (!body.success) return c.json({ error: 'invalid_request', details: body.error.flatten() }, 400)
|
||||||
|
|
||||||
|
const organizationId = instructor.primaryAffiliation?.organizationId
|
||||||
|
if (instructor.role !== 'ADMIN' && (!organizationId || body.data.organizationId !== organizationId)) {
|
||||||
|
return c.json({ error: 'forbidden' }, 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = body.data.email.toLowerCase()
|
||||||
|
const existingUser = await findUserByEmail(email)
|
||||||
|
if (existingUser) return c.json({ error: 'email_already_registered' }, 409)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await createManagedUser({
|
||||||
|
email,
|
||||||
|
password: body.data.password,
|
||||||
|
name: body.data.name,
|
||||||
|
organizationId: body.data.organizationId,
|
||||||
|
department: body.data.department,
|
||||||
|
position: body.data.position,
|
||||||
|
licenseCode: body.data.licenseCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.userAppRole.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
appKey: 'LMS',
|
||||||
|
roleCode: 'STUDENT',
|
||||||
|
assignedById: instructor.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const createdUser = await findUserById(user.id)
|
||||||
|
return c.json({ user: userResponse(createdUser ?? user) }, 201)
|
||||||
|
} catch {
|
||||||
|
return c.json({ error: 'user_create_failed' }, 400)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
authRoutes.post('/admin/users/:userId/affiliations', authRequired, async (c) => {
|
authRoutes.post('/admin/users/:userId/affiliations', authRequired, async (c) => {
|
||||||
if (!requireAdmin(c.get('user'))) return c.json({ error: 'forbidden' }, 403)
|
if (!requireAdmin(c.get('user'))) return c.json({ error: 'forbidden' }, 403)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user