Compare commits

...

2 Commits

Author SHA1 Message Date
chyoo 80715ba98b fix instructor student enrollment authorization
ci/woodpecker/push/woodpecker Pipeline failed
2026-08-13 13:02:27 +09:00
chyoo ef12dffb62 add woddfecker config migrate..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-11 14:16:21 +09:00
2 changed files with 92 additions and 0 deletions
+39
View File
@@ -18,6 +18,45 @@ steps:
branch: release
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:
image: bitnami/kubectl:latest
volumes:
+53
View File
@@ -3,6 +3,7 @@ import { randomInt } from 'crypto'
import { z } from 'zod'
import { config } from '../config'
import { sendEmailVerificationCode } from '../mail'
import { prisma } from '../prisma'
import { verifyPassword } from '../security/password'
import { signAccessToken, verifyAccessToken } from '../security/tokens'
import type { AuthUser } from '../types'
@@ -26,6 +27,7 @@ import {
findOrCreateOAuthUser,
findUserProfileImage,
findUserByEmail,
findUserById,
listOrganizations,
isLoginBlocked,
markLastLogin,
@@ -153,6 +155,14 @@ const userResponse = (user: AuthUser) => ({
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) => ({
accessToken: await signAccessToken(user),
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) => {
if (!requireAdmin(c.get('user'))) return c.json({ error: 'forbidden' }, 403)