Candidate
parent
cfaeb62466
commit
3991bd60a3
|
|
@ -0,0 +1,49 @@
|
|||
import profile
|
||||
from fastapi import APIRouter,Depends, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import HTTPException
|
||||
from db_setup import get_session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel, EmailStr, model_validator
|
||||
from users.views import User
|
||||
from users.permissions import CurrentUser, PermissionTag, require_permission
|
||||
from job_post.views import JobPost
|
||||
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/candidate/cv_upload")
|
||||
async def cv_upload(session: AsyncSession = Depends(get_session),current_user: Depends(require_permission(PermissionTag.CANDIDATES_MANAGE,PermissionTag.CANDIDATES_VIEW,PermissionTag.CANDIDATES_EDIT,PermissionTag.CANDIDATES_DELETE,PermissionTag.CANDIDATES_CREATE))):
|
||||
try:
|
||||
pass
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500,detail=str(e))
|
||||
|
||||
|
||||
@router.post("/job/post-job")
|
||||
async def post_job(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: Depends(require_permission(
|
||||
PermissionTag.JOB_BOARD_CREATE,
|
||||
PermissionTag.JOB_BOARD_VIEW,
|
||||
PermissionTag.JOB_BOARD_APPROVE,
|
||||
PermissionTag.JOB_BOARD_EDIT,
|
||||
PermissionTag.JOB_BOARD_DELETE,
|
||||
PermissionTag.JOB_BOARD_VIEW_ALL,
|
||||
PermissionTag.JOB_BOARD_APPROVE_ALL,
|
||||
PermissionTag.JOB_BOARD_EDIT_ALL,
|
||||
PermissionTag.JOB_BOARD_DELETE_ALL,
|
||||
PermissionTag.JOB_BOARD_VIEW_ALL,
|
||||
PermissionTag.JOB_BOARD_APPROVE_ALL,
|
||||
PermissionTag.JOB_BOARD_EDIT_ALL,
|
||||
PermissionTag.JOB_BOARD_DELETE_ALL,
|
||||
))):
|
||||
try:
|
||||
service=JobPost(session=session)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500,detail=str(e))
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from os import setegid
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.base import state_str
|
||||
from sqlmodel import Field, Relationship, SQLModel, select
|
||||
|
||||
class JobPosts(SQLModel, table=True):
|
||||
__tablename__ = "job_posts"
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
title: str
|
||||
position:str
|
||||
team:str
|
||||
location:str
|
||||
requirements:str
|
||||
responsibilities:str
|
||||
benefits:str
|
||||
salary_range:str
|
||||
description: str
|
||||
created_at: datetime = Field(default_factory=datetime.now)
|
||||
updated_at: datetime = Field(default_factory=datetime.now)
|
||||
is_deleted: bool = Field(default=False)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
class JobPost:
|
||||
def __init__(self,session:AsyncSession):
|
||||
self.session=session
|
||||
self.buffer_api=os.getenv("BUFFER_API")
|
||||
self.client_id=os.getenv("CLIENT_ID")
|
||||
|
||||
async def post_job(self,payload):
|
||||
try:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500,detail=str(e))
|
||||
|
|
@ -9,6 +9,7 @@ from inbox.app import router as inbox_router
|
|||
from users.app import router as users_router
|
||||
from role.app import router as role_router
|
||||
from forget_password.app import router as forget_password_router
|
||||
from job.app import router as candidate_router
|
||||
from notifications.app import router as confirmation_router
|
||||
# Without this the db/migration logs have no handler and are swallowed under uvicorn.
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(name)s: %(message)s")
|
||||
|
|
@ -29,3 +30,4 @@ app.include_router(users_router)
|
|||
app.include_router(role_router)
|
||||
app.include_router(forget_password_router)
|
||||
app.include_router(confirmation_router)
|
||||
app.include_router(candidate_router)
|
||||
|
|
@ -1 +1,3 @@
|
|||
VITE_API_BASE=http://localhost:8000
|
||||
# Use 127.0.0.1, not localhost. On this machine localhost prefers ::1 and hits a
|
||||
# different listener (WSL/Docker on :8000) instead of the Windows uvicorn on 127.0.0.1.
|
||||
VITE_API_BASE=http://127.0.0.1:8000
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
Settings — 10 tabs. Two are real, eight are inert chrome exactly as in the
|
||||
prototype.
|
||||
|
||||
The Users tab is wired to GET /users/fetch, and Appearance drives the real
|
||||
ThemeProvider. Everything else (General, Roles, Permissions, Notifications,
|
||||
Email Templates, Career Portal, Branding, Security) is markup with no
|
||||
persistence — same as the prototype.
|
||||
The Users tab is wired to GET /users/fetch and its row pencil assigns roles
|
||||
through PUT /users/assign-role / PUT /users/remove-role; Appearance drives the
|
||||
real ThemeProvider. Everything else (General, Roles, Permissions,
|
||||
Notifications, Email Templates, Career Portal, Branding, Security) is markup
|
||||
with no persistence — same as the prototype.
|
||||
|
||||
The Security tab in particular renders 2FA and audit logging as ENABLED while
|
||||
enforcing nothing; 01-repository-assessment.md §2.4 calls that out as
|
||||
|
|
@ -14,14 +15,18 @@
|
|||
============================================================ */
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
import Modal from '../ui/Modal'
|
||||
import { Tabs } from '../ui/Tabs'
|
||||
import { Avatar, Badge, Icon } from '../ui/primitives'
|
||||
import { Avatar, Badge, FieldError, Icon } from '../ui/primitives'
|
||||
import { useToast } from '../ui/Toast'
|
||||
import { useTheme } from '../theme/ThemeProvider'
|
||||
import { useFormState } from '../components/AuthLayout'
|
||||
import { usePermission } from '../auth/AuthContext'
|
||||
import { qk } from '../lib/queryKeys'
|
||||
import { friendlyAuthError } from '../lib/errors'
|
||||
import * as rolesApi from '../api/roles'
|
||||
import * as usersApi from '../api/users'
|
||||
import { roles as seedRoles } from '../data/seed'
|
||||
|
||||
|
|
@ -122,6 +127,7 @@ function General() {
|
|||
/** Real data: GET /users/fetch (requires rbac_users.view). */
|
||||
function Users() {
|
||||
const { toast } = useToast()
|
||||
const [editing, setEditing] = useState(null)
|
||||
const usersQuery = useQuery({
|
||||
queryKey: qk.users.list(),
|
||||
queryFn: () => usersApi.list({ top: 50 }).then((r) => r.data ?? []),
|
||||
|
|
@ -180,7 +186,11 @@ function Users() {
|
|||
<td className="text-muted">{u.created_at ? String(u.created_at).slice(0, 10) : '—'}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<div className="row-actions">
|
||||
<button className="act-btn" onClick={() => toast(`Editing ${u.name}`, 'info')}>
|
||||
<button
|
||||
className="act-btn"
|
||||
onClick={() => setEditing(u)}
|
||||
aria-label={`Edit ${u.name}`}
|
||||
>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -191,10 +201,164 @@ function Users() {
|
|||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<AssignRoleModal user={editing} users={users} onClose={() => setEditing(null)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The Users-tab row pencil. Resolves the name from user_id and the current role
|
||||
* from role_id, and persists a change through PUT /users/assign-role — or
|
||||
* /users/remove-role when the role is cleared.
|
||||
*
|
||||
* SINGLE select, deliberately: `users.role_id` is one nullable FK and
|
||||
* Users.update_user does `setattr(user, 'role_id', v)`, so N roles written in a
|
||||
* loop would leave only the last one. Multi-role needs a user_roles join table.
|
||||
*
|
||||
* Saving needs rbac_users.manage on top of the route's rbac_users.edit, and the
|
||||
* server refuses to hand out permissions the caller does not already hold. Both
|
||||
* come back as 403 detail strings, which friendlyAuthError surfaces verbatim.
|
||||
*/
|
||||
function AssignRoleModal({ user, users, onClose }) {
|
||||
const { toast } = useToast()
|
||||
const { can } = usePermission()
|
||||
const qc = useQueryClient()
|
||||
|
||||
const form = useFormState({
|
||||
user_id: String(user.id),
|
||||
role_id: user.role_id == null ? '' : String(user.role_id),
|
||||
})
|
||||
|
||||
// Same key as Access Control, so this is served from cache after visiting it.
|
||||
const rolesQuery = useQuery({
|
||||
queryKey: qk.roles.list(),
|
||||
queryFn: () => rolesApi.listRoles().then((r) => r.data ?? []),
|
||||
})
|
||||
|
||||
// Inactive roles are an option the server can only 400 on — see the is_active
|
||||
// check in users/views.py _check_role_assignment.
|
||||
const roles = (rolesQuery.data ?? []).filter((r) => r.is_active)
|
||||
|
||||
const target = users.find((u) => String(u.id) === form.values.user_id) ?? user
|
||||
const currentRoleId = target.role_id == null ? '' : String(target.role_id)
|
||||
const clearing = form.values.role_id === ''
|
||||
const dirty = form.values.role_id !== currentRoleId
|
||||
|
||||
// Re-point the role select at THAT user's role, so the two fields can never
|
||||
// end up describing different people.
|
||||
function pickUser(id) {
|
||||
const next = users.find((u) => String(u.id) === id)
|
||||
form.setValues({
|
||||
user_id: id,
|
||||
role_id: next?.role_id == null ? '' : String(next.role_id),
|
||||
})
|
||||
}
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () =>
|
||||
clearing
|
||||
? usersApi.removeRole(form.values.user_id)
|
||||
: usersApi.assignRole(form.values.user_id, Number(form.values.role_id)),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: qk.users.all() })
|
||||
const picked = roles.find((r) => String(r.id) === form.values.role_id)
|
||||
toast(
|
||||
clearing
|
||||
? `Role removed from ${target.name}`
|
||||
: `${target.name} is now ${picked?.role_name ?? 'assigned'}`,
|
||||
'success',
|
||||
)
|
||||
onClose()
|
||||
},
|
||||
onError: (err) => toast(friendlyAuthError(err, 'Could not update the role.'), 'error'),
|
||||
})
|
||||
|
||||
const busy = save.isPending
|
||||
|
||||
function submit() {
|
||||
if (!dirty) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
save.mutate()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Assign Role"
|
||||
subtitle={`Change which role ${target.name} holds`}
|
||||
onClose={onClose}
|
||||
footer={
|
||||
<>
|
||||
<button className="btn btn-secondary" onClick={onClose} disabled={busy}>Cancel</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={submit}
|
||||
disabled={busy || rolesQuery.isPending}
|
||||
>
|
||||
<Icon name="check" /> {busy ? 'Saving…' : 'Save Changes'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form noValidate onSubmit={(e) => { e.preventDefault(); submit() }}>
|
||||
<div className="form-grid">
|
||||
<div className="form-field col-span-2">
|
||||
<label htmlFor="ar-user">User</label>
|
||||
<select
|
||||
id="ar-user"
|
||||
value={form.values.user_id}
|
||||
onChange={(e) => pickUser(e.target.value)}
|
||||
disabled={busy}
|
||||
>
|
||||
{users.map((u) => (
|
||||
<option key={u.id} value={u.id}>{u.name} — {u.email}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-field col-span-2">
|
||||
<label htmlFor="ar-role">Role</label>
|
||||
<select
|
||||
id="ar-role"
|
||||
className={rolesQuery.isError ? 'err' : ''}
|
||||
value={form.values.role_id}
|
||||
onChange={(e) => form.setField('role_id', e.target.value)}
|
||||
disabled={busy || rolesQuery.isPending || rolesQuery.isError}
|
||||
>
|
||||
<option value="">No role</option>
|
||||
{roles.map((r) => (
|
||||
<option key={r.id} value={r.id}>{r.role_name}</option>
|
||||
))}
|
||||
</select>
|
||||
<FieldError>
|
||||
{rolesQuery.isError
|
||||
? friendlyAuthError(rolesQuery.error, 'Could not load the role list.')
|
||||
: null}
|
||||
</FieldError>
|
||||
{rolesQuery.isPending && <span className="lr-sub">Loading roles…</span>}
|
||||
{clearing && currentRoleId !== '' && (
|
||||
<span className="lr-sub">
|
||||
Saving will clear this user’s role. They keep no permissions until reassigned.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!can('rbac_users.manage') && (
|
||||
<div className="alert alert-danger" style={{ marginTop: 14 }}>
|
||||
Your account does not hold <code>rbac_users.manage</code>, which the server requires on
|
||||
top of <code>rbac_users.edit</code> to change a role. Saving will be rejected.
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function Roles() {
|
||||
const { toast } = useToast()
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in New Issue