@@ -36,6 +36,23 @@ export const docFeedbackStatusEnum = pgEnum('doc_feedback_status', [
3636 'denied' ,
3737] )
3838export const bannerScopeEnum = pgEnum ( 'banner_scope' , [ 'global' , 'targeted' ] )
39+ export const auditActionEnum = pgEnum ( 'audit_action' , [
40+ 'user.capabilities.update' ,
41+ 'user.adsDisabled.update' ,
42+ 'user.sessions.revoke' ,
43+ 'role.create' ,
44+ 'role.update' ,
45+ 'role.delete' ,
46+ 'role.assignment.create' ,
47+ 'role.assignment.delete' ,
48+ 'banner.create' ,
49+ 'banner.update' ,
50+ 'banner.delete' ,
51+ 'feed.entry.create' ,
52+ 'feed.entry.update' ,
53+ 'feed.entry.delete' ,
54+ 'feedback.moderate' ,
55+ ] )
3956export const bannerStyleEnum = pgEnum ( 'banner_style' , [
4057 'info' ,
4158 'warning' ,
@@ -62,6 +79,22 @@ export type DocFeedbackStatus = 'pending' | 'approved' | 'denied'
6279export type BannerScope = 'global' | 'targeted'
6380export type BannerStyle = 'info' | 'warning' | 'success' | 'promo'
6481export type EntryType = 'release' | 'blog' | 'announcement'
82+ export type AuditAction =
83+ | 'user.capabilities.update'
84+ | 'user.adsDisabled.update'
85+ | 'user.sessions.revoke'
86+ | 'role.create'
87+ | 'role.update'
88+ | 'role.delete'
89+ | 'role.assignment.create'
90+ | 'role.assignment.delete'
91+ | 'banner.create'
92+ | 'banner.update'
93+ | 'banner.delete'
94+ | 'feed.entry.create'
95+ | 'feed.entry.update'
96+ | 'feed.entry.delete'
97+ | 'feedback.moderate'
6598
6699// Constants
67100export const VALID_CAPABILITIES : readonly Capability [ ] = [
@@ -678,6 +711,67 @@ export type NewAnnouncementDismissal = InferInsertModel<
678711 typeof announcementDismissals
679712>
680713
714+ // Login history table (tracks user logins for analytics and security)
715+ export const loginHistory = pgTable (
716+ 'login_history' ,
717+ {
718+ id : uuid ( 'id' ) . primaryKey ( ) . defaultRandom ( ) ,
719+ userId : uuid ( 'user_id' )
720+ . notNull ( )
721+ . references ( ( ) => users . id , { onDelete : 'cascade' } ) ,
722+ provider : oauthProviderEnum ( 'provider' ) . notNull ( ) ,
723+ ipAddress : varchar ( 'ip_address' , { length : 45 } ) , // IPv6 max length
724+ userAgent : text ( 'user_agent' ) ,
725+ isNewUser : boolean ( 'is_new_user' ) . notNull ( ) . default ( false ) ,
726+ createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'date' } )
727+ . notNull ( )
728+ . defaultNow ( ) ,
729+ } ,
730+ ( table ) => ( {
731+ userIdIdx : index ( 'login_history_user_id_idx' ) . on ( table . userId ) ,
732+ createdAtIdx : index ( 'login_history_created_at_idx' ) . on ( table . createdAt ) ,
733+ providerIdx : index ( 'login_history_provider_idx' ) . on ( table . provider ) ,
734+ } ) ,
735+ )
736+
737+ export type LoginHistory = InferSelectModel < typeof loginHistory >
738+ export type NewLoginHistory = InferInsertModel < typeof loginHistory >
739+
740+ // Audit logs table (tracks admin actions for security and compliance)
741+ export const auditLogs = pgTable (
742+ 'audit_logs' ,
743+ {
744+ id : uuid ( 'id' ) . primaryKey ( ) . defaultRandom ( ) ,
745+ // Who performed the action
746+ actorId : uuid ( 'actor_id' )
747+ . notNull ( )
748+ . references ( ( ) => users . id , { onDelete : 'cascade' } ) ,
749+ // What action was performed
750+ action : auditActionEnum ( 'action' ) . notNull ( ) ,
751+ // Target of the action (user, role, banner, etc.)
752+ targetType : varchar ( 'target_type' , { length : 50 } ) . notNull ( ) , // 'user', 'role', 'banner', 'feed_entry', 'feedback'
753+ targetId : varchar ( 'target_id' , { length : 255 } ) . notNull ( ) , // UUID or other identifier
754+ // Details of the change
755+ details : jsonb ( 'details' ) , // { before: {...}, after: {...} } or other relevant data
756+ // Request metadata
757+ ipAddress : varchar ( 'ip_address' , { length : 45 } ) ,
758+ userAgent : text ( 'user_agent' ) ,
759+ createdAt : timestamp ( 'created_at' , { withTimezone : true , mode : 'date' } )
760+ . notNull ( )
761+ . defaultNow ( ) ,
762+ } ,
763+ ( table ) => ( {
764+ actorIdIdx : index ( 'audit_logs_actor_id_idx' ) . on ( table . actorId ) ,
765+ actionIdx : index ( 'audit_logs_action_idx' ) . on ( table . action ) ,
766+ targetTypeIdx : index ( 'audit_logs_target_type_idx' ) . on ( table . targetType ) ,
767+ targetIdIdx : index ( 'audit_logs_target_id_idx' ) . on ( table . targetId ) ,
768+ createdAtIdx : index ( 'audit_logs_created_at_idx' ) . on ( table . createdAt ) ,
769+ } ) ,
770+ )
771+
772+ export type AuditLog = InferSelectModel < typeof auditLogs >
773+ export type NewAuditLog = InferInsertModel < typeof auditLogs >
774+
681775// Relations
682776export const usersRelations = relations ( users , ( { many } ) => ( {
683777 sessions : many ( sessions ) ,
@@ -686,6 +780,8 @@ export const usersRelations = relations(users, ({ many }) => ({
686780 docFeedback : many ( docFeedback ) ,
687781 announcementDismissals : many ( announcementDismissals ) ,
688782 bannerDismissals : many ( bannerDismissals ) ,
783+ loginHistory : many ( loginHistory ) ,
784+ auditLogs : many ( auditLogs ) ,
689785} ) )
690786
691787export const rolesRelations = relations ( roles , ( { many } ) => ( {
@@ -758,3 +854,17 @@ export const bannerDismissalsRelations = relations(
758854 } ) ,
759855 } ) ,
760856)
857+
858+ export const loginHistoryRelations = relations ( loginHistory , ( { one } ) => ( {
859+ user : one ( users , {
860+ fields : [ loginHistory . userId ] ,
861+ references : [ users . id ] ,
862+ } ) ,
863+ } ) )
864+
865+ export const auditLogsRelations = relations ( auditLogs , ( { one } ) => ( {
866+ actor : one ( users , {
867+ fields : [ auditLogs . actorId ] ,
868+ references : [ users . id ] ,
869+ } ) ,
870+ } ) )
0 commit comments