Candidate

pull/2/head
ahmed.mujtaba 2026-08-05 18:25:34 +05:00
parent cfaeb62466
commit 3991bd60a3
6 changed files with 268 additions and 9 deletions

49
backend/job/app.py Normal file
View File

@ -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))

View File

@ -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)

View File

@ -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))

View File

@ -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")
@ -28,4 +29,5 @@ app.include_router(inbox_router)
app.include_router(users_router)
app.include_router(role_router)
app.include_router(forget_password_router)
app.include_router(confirmation_router)
app.include_router(confirmation_router)
app.include_router(candidate_router)

View File

@ -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

View File

@ -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 users 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 (