Initial Phoenix Sankofa Cloud setup
- Complete project structure with Next.js frontend - GraphQL API backend with Apollo Server - Portal application with NextAuth - Crossplane Proxmox provider - GitOps configurations - CI/CD pipelines - Testing infrastructure (Vitest, Jest, Go tests) - Error handling and monitoring - Security hardening - UI component library - Documentation
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema'
|
||||
import { typeDefs } from './typeDefs'
|
||||
import { resolvers } from './resolvers'
|
||||
|
||||
export const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { Context } from '../types/context'
|
||||
import * as resourceService from '../services/resource'
|
||||
import * as siteService from '../services/site'
|
||||
import * as userService from '../services/user'
|
||||
import * as authService from '../services/auth'
|
||||
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
health: () => ({
|
||||
status: 'ok',
|
||||
timestamp: new Date(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
}),
|
||||
|
||||
resources: async (_: unknown, args: { filter?: any }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return resourceService.getResources(context, args.filter)
|
||||
},
|
||||
|
||||
resource: async (_: unknown, args: { id: string }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return resourceService.getResource(context, args.id)
|
||||
},
|
||||
|
||||
sites: async (_: unknown, __: unknown, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return siteService.getSites(context)
|
||||
},
|
||||
|
||||
site: async (_: unknown, args: { id: string }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return siteService.getSite(context, args.id)
|
||||
},
|
||||
|
||||
me: async (_: unknown, __: unknown, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return context.user
|
||||
},
|
||||
|
||||
users: async (_: unknown, __: unknown, context: Context) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new GraphQLError('Admin access required', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
return userService.getUsers(context)
|
||||
},
|
||||
|
||||
user: async (_: unknown, args: { id: string }, context: Context) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new GraphQLError('Admin access required', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
return userService.getUser(context, args.id)
|
||||
},
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
login: async (_: unknown, args: { email: string; password: string }) => {
|
||||
return authService.login(args.email, args.password)
|
||||
},
|
||||
|
||||
logout: async (_: unknown, __: unknown, _context: Context) => {
|
||||
// In a real implementation, invalidate the token
|
||||
return true
|
||||
},
|
||||
|
||||
createResource: async (
|
||||
_: unknown,
|
||||
args: { input: any },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return resourceService.createResource(context, args.input)
|
||||
},
|
||||
|
||||
updateResource: async (
|
||||
_: unknown,
|
||||
args: { id: string; input: any },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return resourceService.updateResource(context, args.id, args.input)
|
||||
},
|
||||
|
||||
deleteResource: async (
|
||||
_: unknown,
|
||||
args: { id: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Authentication required', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
return resourceService.deleteResource(context, args.id)
|
||||
},
|
||||
|
||||
createUser: async (
|
||||
_: unknown,
|
||||
args: { input: any },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new GraphQLError('Admin access required', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
return userService.createUser(context, args.input)
|
||||
},
|
||||
|
||||
updateUser: async (
|
||||
_: unknown,
|
||||
args: { id: string; input: any },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new GraphQLError('Admin access required', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
return userService.updateUser(context, args.id, args.input)
|
||||
},
|
||||
|
||||
deleteUser: async (
|
||||
_: unknown,
|
||||
args: { id: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user || context.user.role !== 'ADMIN') {
|
||||
throw new GraphQLError('Admin access required', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
return userService.deleteUser(context, args.id)
|
||||
},
|
||||
},
|
||||
|
||||
Resource: {
|
||||
site: async (resource: any, __: unknown, context: Context) => {
|
||||
return siteService.getSite(context, resource.siteId)
|
||||
},
|
||||
},
|
||||
|
||||
Site: {
|
||||
resources: async (site: any, __: unknown, context: Context) => {
|
||||
return resourceService.getResources(context, { siteId: site.id })
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
|
||||
export const typeDefs = gql`
|
||||
scalar DateTime
|
||||
scalar JSON
|
||||
|
||||
type Query {
|
||||
# Health check
|
||||
health: HealthStatus
|
||||
|
||||
# Resources
|
||||
resources(filter: ResourceFilter): [Resource!]!
|
||||
resource(id: ID!): Resource
|
||||
|
||||
# Sites
|
||||
sites: [Site!]!
|
||||
site(id: ID!): Site
|
||||
|
||||
# Users
|
||||
me: User
|
||||
users: [User!]!
|
||||
user(id: ID!): User
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# Authentication
|
||||
login(email: String!, password: String!): AuthPayload!
|
||||
logout: Boolean!
|
||||
|
||||
# Resources
|
||||
createResource(input: CreateResourceInput!): Resource!
|
||||
updateResource(id: ID!, input: UpdateResourceInput!): Resource!
|
||||
deleteResource(id: ID!): Boolean!
|
||||
|
||||
# Users
|
||||
createUser(input: CreateUserInput!): User!
|
||||
updateUser(id: ID!, input: UpdateUserInput!): User!
|
||||
deleteUser(id: ID!): Boolean!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
resourceUpdated(id: ID!): Resource!
|
||||
resourceCreated: Resource!
|
||||
resourceDeleted(id: ID!): ID!
|
||||
}
|
||||
|
||||
type HealthStatus {
|
||||
status: String!
|
||||
timestamp: DateTime!
|
||||
version: String!
|
||||
}
|
||||
|
||||
type Resource {
|
||||
id: ID!
|
||||
name: String!
|
||||
type: ResourceType!
|
||||
status: ResourceStatus!
|
||||
site: Site!
|
||||
metadata: JSON
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Site {
|
||||
id: ID!
|
||||
name: String!
|
||||
region: String!
|
||||
status: SiteStatus!
|
||||
resources: [Resource!]!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
name: String!
|
||||
role: UserRole!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type AuthPayload {
|
||||
token: String!
|
||||
user: User!
|
||||
}
|
||||
|
||||
enum ResourceType {
|
||||
VM
|
||||
CONTAINER
|
||||
STORAGE
|
||||
NETWORK
|
||||
}
|
||||
|
||||
enum ResourceStatus {
|
||||
PENDING
|
||||
PROVISIONING
|
||||
RUNNING
|
||||
STOPPED
|
||||
ERROR
|
||||
DELETING
|
||||
}
|
||||
|
||||
enum SiteStatus {
|
||||
ACTIVE
|
||||
INACTIVE
|
||||
MAINTENANCE
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
ADMIN
|
||||
USER
|
||||
VIEWER
|
||||
}
|
||||
|
||||
input ResourceFilter {
|
||||
type: ResourceType
|
||||
status: ResourceStatus
|
||||
siteId: ID
|
||||
}
|
||||
|
||||
input CreateResourceInput {
|
||||
name: String!
|
||||
type: ResourceType!
|
||||
siteId: ID!
|
||||
metadata: JSON
|
||||
}
|
||||
|
||||
input UpdateResourceInput {
|
||||
name: String
|
||||
metadata: JSON
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
email: String!
|
||||
name: String!
|
||||
password: String!
|
||||
role: UserRole
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
name: String
|
||||
role: UserRole
|
||||
}
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user